Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,19 @@ External scenario modules own one specific screenshot's data, custom CSS, and
browser steps while reusing this runner's temporary site and high-resolution
capture. Pass their path with `--scenario`; the scenario's `name` sets the PNG
filename directly inside `--output`. A scenario can set its own viewport when a
wider layout is part of the screenshot.
wider layout is part of the screenshot. Set `adminSetup: false` when the
scenario needs to capture the site before setup. The runner skips admin setup,
Stripe, and the saved theme, then applies the scenario's CSS after its browser
steps.

Pass `--layers` with an external scenario to also write transparent background,
controls, and text layers. They are named
`<scenario-name>__layer-<layer>.png` next to the normal screenshot.

```bash
deno task screenshot --scenario ../tickets-site/scripts/screenshots/charity-events.js \
--layers --output ../tickets-site/images/screenshots
```

Pass `--social` (one of `facebook`, `instagram-landscape`,
`instagram-portrait`, `instagram-square`, a comma-separated list, or `all`) to
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test": "deno run --v8-flags=--expose-gc -A scripts/run-tests.ts",
"test:coverage": "deno run --v8-flags=--expose-gc -A scripts/run-tests.ts --coverage",
"test:files": "deno run --v8-flags=--expose-gc -A scripts/run-test-files.ts",
"test:screenshot-contract": "deno test --no-check -A test/scripts/screenshots-browser.contract.ts",
"test:screenshot-contract": "deno test --no-check -A test/scripts/*-browser.contract.ts",
"specs": "deno run --v8-flags=--expose-gc -A scripts/run-specs.ts",
"specs:evidence": "deno run --v8-flags=--expose-gc -A scripts/run-spec-evidence.ts",
"specs:check": "deno run --allow-read scripts/check-specs.ts",
Expand Down
11 changes: 7 additions & 4 deletions scripts/browser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ export const browserLaunchOptions = (
const launchScreenshotChromium = (
browser: Chromium,
executablePath?: string,
overrides: Pick<LaunchOptions, "ignoreDefaultArgs"> = {},
): Promise<Browser> =>
browser.launch(
browserLaunchOptions(true, executablePath, SCREENSHOT_LAUNCH_ARGS),
);
browser.launch({
...browserLaunchOptions(true, executablePath, SCREENSHOT_LAUNCH_ARGS),
...overrides,
});

export const defineScreenshotBrowserLauncher =
(
browser: Chromium,
executablePath: () => Promise<string | undefined>,
overrides?: Pick<LaunchOptions, "ignoreDefaultArgs">,
): (() => Promise<Browser>) =>
async () =>
launchScreenshotChromium(browser, await executablePath());
launchScreenshotChromium(browser, await executablePath(), overrides);
73 changes: 53 additions & 20 deletions scripts/screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import {
startScreenshotAppServer,
} from "./screenshots/app-server.ts";
import { chromiumExecutable } from "./screenshots/browser.ts";
import { capturePreparedPage } from "./screenshots/capture.ts";
import {
capturePreparedLayers,
capturePreparedPage,
installLayerCaptureClock,
} from "./screenshots/capture.ts";
import { isCompactWidth, isolateElementCss } from "./screenshots/checks.ts";
import type { Rgb } from "./screenshots/color.ts";
import {
addScreenshotStyle,
SCREENSHOT_LAYER_NAMES,
} from "./screenshots/layers.ts";
import {
parseScreenshotOptions,
type ScreenshotName,
Expand Down Expand Up @@ -252,20 +260,17 @@ const captureScenario = async (
server: ScreenshotAppServer,
outputDir: string,
social: readonly SocialTargetName[] = [],
layers = false,
): Promise<void> => {
const { baseUrl } = server;
await setupAdmin(page, baseUrl, scenario.setupUsername);
await server.enableStripe();
await applyTheme(
page,
baseUrl,
"default",
`${scenario.css}\n${
scenario.elementSelector
? isolateElementCss(scenario.elementSelector)
: ""
}`,
);
const captureCss = `${scenario.css}\n${
scenario.elementSelector ? isolateElementCss(scenario.elementSelector) : ""
}`;
if (scenario.adminSetup !== false) {
await setupAdmin(page, baseUrl, scenario.setupUsername);
await server.enableStripe();
Comment thread
stefan-burke marked this conversation as resolved.
Resolved
await applyTheme(page, baseUrl, "default", captureCss);
}
await scenario.run({
balancePathFor: async (attendeeId) => {
const { signBalanceToken } = await import("#shared/balance-link.ts");
Expand All @@ -275,15 +280,35 @@ const captureScenario = async (
page,
submit: (formSelector) => submit(page, formSelector),
});
if (scenario.adminSetup === false) {
await addScreenshotStyle(page, captureCss);
}
await waitForScreenshotPage(page);
const outputPath = join(outputDir, `${scenario.name}.png`);
const background = await captureAndWrite(
page,
outputPath,
scenario.elementSelector,
scenario.fullPage,
);
const layered = layers
? await capturePreparedLayers(
page,
scenario.elementSelector,
scenario.fullPage,
)
: undefined;
const background = layered
? layered.background
: await captureAndWrite(
page,
outputPath,
scenario.elementSelector,
scenario.fullPage,
);
if (layered) await Deno.writeFile(outputPath, layered.png);
console.log(`${scenario.name}.png`);
if (layered) {
for (const layer of SCREENSHOT_LAYER_NAMES) {
const name = `${scenario.name}__layer-${layer}.png`;
await Deno.writeFile(join(outputDir, name), layered.layers[layer]);
console.log(name);
}
}
await writeSocialVariants(
outputPath,
outputDir,
Expand All @@ -294,6 +319,12 @@ const captureScenario = async (
);
};

const installLayerClock = (page: Page, layers: boolean): Promise<void> =>
layers ? installLayerCaptureClock(page) : Promise.resolve();

const elementCaptureCss = (selector: string | undefined): string =>
selector ? isolateElementCss(selector) : "";

const main = async (): Promise<void> => {
Deno.env.set("PW_TEST_SCREENSHOT_NO_FONTS_READY", "1");
const options = parseScreenshotOptions(Deno.args);
Expand All @@ -311,6 +342,7 @@ const main = async (): Promise<void> => {
...screenshotContextOptions(MOBILE_SCREENSHOT_PROFILE),
});
const page = await context.newPage();
await installLayerClock(page, options.layers ?? false);
page.setDefaultTimeout(TIMEOUT_MS);
if (scenario) {
await captureScenario(
Expand All @@ -319,6 +351,7 @@ const main = async (): Promise<void> => {
server,
outputDir,
options.social ?? [],
options.layers ?? false,
);
return;
}
Expand All @@ -333,7 +366,7 @@ const main = async (): Promise<void> => {
page,
server.baseUrl,
theme,
elementSelector ? isolateElementCss(elementSelector) : "",
elementCaptureCss(elementSelector),
);
const outputPath = join(themeDir, `${name}.png`);
const background = await capture(
Expand Down
27 changes: 18 additions & 9 deletions scripts/screenshots/app-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
} from "#scripts/process.ts";
import {
type StartupCleanup,
startOnFirstUse,
startWithFailureCleanup,
waitForHealthy,
} from "#scripts/screenshots/server.ts";
import {
findAvailablePort,
reserveAvailablePort,
startStripeMock,
stripeMockEnv,
} from "#scripts/stripe-mock.ts";
Expand All @@ -33,25 +34,32 @@ const startAppServer = async ({
add,
run,
}: StartupCleanup): Promise<ScreenshotAppServer> => {
const stripeMock = await startStripeMock();
add(stripeMock.stop);
const stripePort = reserveAvailablePort();
add(stripePort.release);
const enableStripeMock = startOnFirstUse(async () => {
stripePort.release();
return await startStripeMock({ port: stripePort.port });
}, add);
const tempDir = await Deno.makeTempDir({ prefix: "tickets-screenshots-" });
add(() => removeTree(tempDir));
const port = findAvailablePort();
const baseUrl = `http://127.0.0.1:${port}`;
const appPort = reserveAvailablePort();
add(appPort.release);
const baseUrl = `http://127.0.0.1:${appPort.port}`;
const dbUrl = `file:${join(tempDir, "screenshots.db")}`;
const child = denoCommand(["run", "-A", "src/index.ts"], {
const command = denoCommand(["run", "-A", "src/index.ts"], {
cwd: ROOT,
env: {
...Deno.env.toObject(),
...stripeMockEnv(stripeMock.port),
...stripeMockEnv(stripePort.port),
DB_ENCRYPTION_KEY: DB_KEY,
DB_URL: dbUrl,
PORT: String(port),
PORT: String(appPort.port),
},
stderr: "inherit",
stdout: "null",
}).spawn();
});
appPort.release();
const child = command.spawn();
add(() => stopProcess(child, STOP_TIMEOUT_MS));

const deadline = Date.now() + TIMEOUT_MS;
Expand All @@ -66,6 +74,7 @@ const startAppServer = async ({
return {
baseUrl,
enableStripe: async () => {
await enableStripeMock();
Deno.env.set("DB_ENCRYPTION_KEY", DB_KEY);
Deno.env.set("DB_URL", dbUrl);
const { settings } = await import("#shared/db/settings.ts");
Expand Down
Loading