Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --playwright & --cypress flags #882

Merged
merged 11 commits into from
Jan 24, 2024
4 changes: 4 additions & 0 deletions action-src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async function run() {
const branchName = getInput('branchName');
const buildScriptName = getInput('buildScriptName');
const configFile = getInput('configFile');
const cypress = getInput('cypress');
const debug = getInput('debug');
const diagnosticsFile = getInput('diagnosticsFile') || getInput('diagnostics');
const dryRun = getInput('dryRun');
Expand All @@ -107,6 +108,7 @@ async function run() {
const onlyChanged = getInput('onlyChanged');
const onlyStoryFiles = getInput('onlyStoryFiles');
const onlyStoryNames = getInput('onlyStoryNames');
const playwright = getInput('playwright');
const preserveMissing = getInput('preserveMissing');
const projectToken = getInput('projectToken') || getInput('appCode'); // backwards compatibility
const repositorySlug = getInput('repositorySlug');
Expand Down Expand Up @@ -139,6 +141,7 @@ async function run() {
branchName: maybe(branchName),
buildScriptName: maybe(buildScriptName),
configFile: maybe(configFile),
cypress: maybe(cypress),
debug: maybe(debug),
diagnosticsFile: maybe(diagnosticsFile),
dryRun: maybe(dryRun),
Expand All @@ -153,6 +156,7 @@ async function run() {
onlyChanged: maybe(onlyChanged),
onlyStoryFiles: maybe(onlyStoryFiles),
onlyStoryNames: maybe(onlyStoryNames),
playwright: maybe(playwright),
preserveMissing: maybe(preserveMissing),
projectToken,
repositorySlug: maybe(repositorySlug),
Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ inputs:
required: false
configFile:
description: 'Path from where to load the Chromatic config JSON file.'
cypress:
description: 'Run build against `chromatic-cypress` test archives'
required: false
debug:
description: 'Output verbose debugging information'
Expand Down Expand Up @@ -66,6 +68,9 @@ inputs:
onlyStoryFiles:
description: 'Only run a single story or a subset of stories by their filename(s)'
required: false
playwright:
description: 'Run build against `chromatic-playwright` test archives'
required: false
preserveMissing:
description: 'Deprecated, use onlyChanged, onlyStoryNames or onlyStoryFiles instead'
required: false
Expand Down
40 changes: 35 additions & 5 deletions node-src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dns from 'dns';
import { execa as execaDefault } from 'execa';
import { execaCommand as execaDefault } from 'execa';
import jsonfile from 'jsonfile';
import { confirm } from 'node-ask';
import fetchDefault from 'node-fetch';
Expand Down Expand Up @@ -38,7 +38,11 @@ beforeEach(() => {
vi.mock('dns');
vi.mock('execa');

const execa = vi.mocked(execaDefault);
vi.mock('./lib/getE2eBinPath', () => ({
getE2eBinPath: () => 'path/to/bin',
}));

const execaCommand = vi.mocked(execaDefault);
const fetch = vi.mocked(fetchDefault);
const upload = vi.mocked(uploadFiles);

Expand Down Expand Up @@ -334,8 +338,8 @@ beforeEach(() => {
CHROMATIC_APP_CODE: undefined,
CHROMATIC_PROJECT_TOKEN: undefined,
};
execa.mockReset();
execa.mockResolvedValue({ stdout: '1.2.3' } as any);
execaCommand.mockReset();
execaCommand.mockResolvedValue({ stdout: '1.2.3' } as any);
getCommit.mockResolvedValue({
commit: 'commit',
committedAt: 1234,
Expand Down Expand Up @@ -466,6 +470,12 @@ it('should exit with code 6 and stop the build when abortSignal is aborted', asy
it('calls out to npm build script passed and uploads files', async () => {
const ctx = getContext(['--project-token=asdf1234', '--build-script-name=build-storybook']);
await runAll(ctx);

expect(execaCommand).toHaveBeenCalledWith(
expect.stringMatching(/build-storybook/),
expect.objectContaining({})
);

expect(ctx.exitCode).toBe(1);
expect(uploadFiles).toHaveBeenCalledWith(
expect.any(Object),
Expand Down Expand Up @@ -501,7 +511,7 @@ it('skips building and uploads directly with storybook-build-dir', async () => {
const ctx = getContext(['--project-token=asdf1234', '--storybook-build-dir=dirname']);
await runAll(ctx);
expect(ctx.exitCode).toBe(1);
expect(execa).not.toHaveBeenCalled();
expect(execaCommand).not.toHaveBeenCalled();
expect(uploadFiles).toHaveBeenCalledWith(
expect.any(Object),
[
Expand Down Expand Up @@ -532,6 +542,26 @@ it('skips building and uploads directly with storybook-build-dir', async () => {
);
});

it('builds with playwright with --playwright', async () => {
const ctx = getContext(['--project-token=asdf1234', '--playwright']);
await runAll(ctx);
expect(execaCommand).toHaveBeenCalledWith(
expect.stringMatching(/path\/to\/bin/),
expect.objectContaining({})
);
expect(ctx.exitCode).toBe(1);
});

it('builds with cypress with --cypress', async () => {
const ctx = getContext(['--project-token=asdf1234', '--cypress']);
await runAll(ctx);
expect(execaCommand).toHaveBeenCalledWith(
expect.stringMatching(/path\/to\/bin/),
expect.objectContaining({})
);
expect(ctx.exitCode).toBe(1);
});

it('passes autoAcceptChanges to the index', async () => {
const ctx = getContext(['--project-token=asdf1234', '--auto-accept-changes']);
await runAll(ctx);
Expand Down
2 changes: 2 additions & 0 deletions node-src/lib/getConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const configurationSchema = z
ignoreLastBuildOnBranch: z.string(),

buildScriptName: z.string(),
playwright: z.boolean(),
cypress: z.boolean(),
outputDir: z.string(),
skip: z.union([z.string(), z.boolean()]),

Expand Down
18 changes: 18 additions & 0 deletions node-src/lib/getE2eBinPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Context } from '../types';
import missingDependency from '../ui/messages/errors/missingDependency';
import { exitCodes, setExitCode } from './setExitCode';
import { failed } from '../ui/tasks/build';

export function getE2eBinPath(ctx: Context, flag: 'playwright' | 'cypress') {
const dependencyName = `chromatic-${flag}`;
try {
return require.resolve(`${dependencyName}/bin/build-archive-storybook`);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
ctx.log.error(missingDependency({ dependencyName, flag }));
setExitCode(ctx, exitCodes.MISSING_DEPENDENCY, true);
throw new Error(failed(ctx).output);
}
throw err;
}
}
tmeasday marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions node-src/lib/getOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export default function getOptions({
preserveMissingSpecs: undefined,

buildScriptName: undefined,
playwright: undefined,
cypress: undefined,
outputDir: undefined,
allowConsoleErrors: undefined,
storybookBuildDir: undefined,
Expand Down Expand Up @@ -128,6 +130,8 @@ export default function getOptions({
flags.preserveMissing || typeof flags.only === 'string' ? true : undefined,

buildScriptName: flags.buildScriptName,
playwright: trueIfSet(flags.playwright),
cypress: trueIfSet(flags.cypress),
outputDir: takeLast(flags.outputDir),
allowConsoleErrors: flags.allowConsoleErrors,
storybookBuildDir: takeLast(flags.storybookBuildDir),
Expand Down Expand Up @@ -202,6 +206,8 @@ export default function getOptions({
const singularOpts = {
buildScriptName: '--build-script-name',
storybookBuildDir: '--storybook-build-dir',
playwright: '--playwright',
cypress: '--cypress',
};
const foundSingularOpts = Object.keys(singularOpts).filter((name) => !!options[name]);

Expand Down Expand Up @@ -254,6 +260,10 @@ export default function getOptions({
return options;
}

if (options.playwright || options.cypress) {
return options;
}

const { scripts } = packageJson;
if (typeof buildScriptName !== 'string') {
buildScriptName = 'build-storybook';
Expand Down
1 change: 1 addition & 0 deletions node-src/lib/setExitCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const exitCodes = {
// I/O errors
FETCH_ERROR: 201,
GRAPHQL_ERROR: 202,
MISSING_DEPENDENCY: 210,
INVALID_OPTIONS: 254,
};

Expand Down
27 changes: 18 additions & 9 deletions node-src/tasks/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { endActivity, startActivity } from '../ui/components/activity';
import buildFailed from '../ui/messages/errors/buildFailed';
import { failed, initial, pending, skipped, success } from '../ui/tasks/build';
import { getPackageManagerRunCommand } from '../lib/getPackageManager';
import { getE2eBinPath } from '../lib/getE2eBinPath';

export const setSourceDir = async (ctx: Context) => {
if (ctx.options.outputDir) {
Expand All @@ -34,15 +35,22 @@ export const setBuildCommand = async (ctx: Context) => {
ctx.log.warn('Storybook version 6.2.0 or later is required to use the --only-changed flag');
}

ctx.buildCommand = await getPackageManagerRunCommand(
[
const buildCommandOptions = [
'--output-dir',
ctx.sourceDir,
ctx.git.changedFiles && webpackStatsSupported && '--webpack-stats-json',
ctx.git.changedFiles && webpackStatsSupported && ctx.sourceDir,
].filter(Boolean);

if (ctx.options.playwright || ctx.options.cypress) {
const binPath = getE2eBinPath(ctx, ctx.options.playwright ? 'playwright' : 'cypress');
ctx.buildCommand = ['node', binPath, ...buildCommandOptions].join(' ');
} else {
ctx.buildCommand = await getPackageManagerRunCommand([
ctx.options.buildScriptName,
'--output-dir',
ctx.sourceDir,
ctx.git.changedFiles && webpackStatsSupported && '--webpack-stats-json',
ctx.git.changedFiles && webpackStatsSupported && ctx.sourceDir,
].filter(Boolean)
);
...buildCommandOptions,
]);
}
};

const timeoutAfter = (ms) =>
Expand All @@ -64,10 +72,11 @@ export const buildStorybook = async (ctx: Context) => {
ctx.log.debug('Running build command:', ctx.buildCommand);
ctx.log.debug('Runtime metadata:', JSON.stringify(ctx.runtimeMetadata, null, 2));

console.log(ctx.buildCommand);
const subprocess = execaCommand(ctx.buildCommand, {
stdio: [null, logFile, logFile],
signal,
env: { NODE_ENV: ctx.env.STORYBOOK_NODE_ENV || 'production' },
env: { NODE_ENV: ctx.env.STORYBOOK_NODE_ENV || 'production' },
});
await Promise.race([subprocess, timeoutAfter(ctx.env.STORYBOOK_BUILD_TIMEOUT)]);
} catch (e) {
Expand Down
6 changes: 6 additions & 0 deletions node-src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface Flags {
outputDir?: string[];
storybookBuildDir?: string[];

// E2E options
playwright?: boolean;
cypress?: boolean;

// Chromatic options
autoAcceptChanges?: string;
branchName?: string;
Expand Down Expand Up @@ -90,6 +94,8 @@ export interface Options extends Configuration {
originalArgv: string[];

buildScriptName: Flags['buildScriptName'];
playwright: Flags['playwright'];
cypress: Flags['cypress'];
outputDir: string;
allowConsoleErrors: Flags['allowConsoleErrors'];
url?: string;
Expand Down
8 changes: 8 additions & 0 deletions node-src/ui/messages/errors/missingDependency.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import missingDependency from './missingDependency';

export default {
title: 'CLI/Messages/Errors',
};

export const MissingDependency = () =>
missingDependency({ dependencyName: 'chromatic-playwright', flag: 'playwright' });
11 changes: 11 additions & 0 deletions node-src/ui/messages/errors/missingDependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import chalk from 'chalk';
import { dedent } from 'ts-dedent';
import { error, info } from '../../components/icons';

export default ({ dependencyName, flag }: { dependencyName: string; flag: string }) => {
return dedent(chalk`
${error} Failed to import \`${dependencyName}\`, is it installed in \`package.json\`?

${info} To run \`chromatic --${flag}\` you must have \`${dependencyName}\` installed.
`);
};
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@
"zen-observable": "^0.8.15",
"zod": "^3.22.2"
},
"peerDependencies": {
"chromatic-playwright": "^0.3.1",
"chromatic-cypress": "^0.3.2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to cut 1.0.0 versions of these when we go GA, we might want to include that here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, we can do that. It'll still be possible to test (with incorrect versions) against the current versions if we do that, but it'll warn in some package managers, and error in npm: https://www.notion.so/chromatic-ui/Peer-Dependency-Investigations-ae77792bae6e420bbe860916bec10cbe?pvs=4#176332d500194cdfacadf1efd927c815

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I misinterpreted, you mean to have both versions supported 👍

},
"peerDependenciesMeta": {
"chromatic-playwright": {
"optional": true
},
"chromatic-cypress": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
Loading