Skip to content

Commit

Permalink
Merge branch 'storybookjs:next' into fix-deps-on-sharp-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
shuta13 committed Apr 11, 2024
2 parents 2cf3334 + c9fbb76 commit 799416e
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 133 deletions.
38 changes: 14 additions & 24 deletions code/addons/actions/src/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
/* eslint-disable no-underscore-dangle */
import type { LoaderFunction } from '@storybook/types';
import { global } from '@storybook/global';
import type { onMockCall as onMockCallType } from '@storybook/test';
import { action } from './runtime';

export const tinySpyInternalState = Symbol.for('tinyspy:spy');
let subscribed = false;

const attachActionsToFunctionMocks: LoaderFunction = (context) => {
const logActionsWhenMockCalled: LoaderFunction = (context) => {
const {
args,
parameters: { actions },
} = context;
if (actions?.disable) return;

Object.entries(args)
.filter(
([, value]) =>
typeof value === 'function' && '_isMockFunction' in value && value._isMockFunction
)
.forEach(([key, value]) => {
// See this discussion for context:
// https://github.com/vitest-dev/vitest/pull/5352
const previous =
value.getMockImplementation() ??
(tinySpyInternalState in value ? value[tinySpyInternalState]?.getOriginal() : undefined);
if (previous?._actionAttached !== true && previous?.isAction !== true) {
const implementation = (...params: unknown[]) => {
action(key)(...params);
return previous?.(...params);
};
implementation._actionAttached = true;
value.mockImplementation(implementation);
}
});
if (
!subscribed &&
'__STORYBOOK_TEST_ON_MOCK_CALL__' in global &&
typeof global.__STORYBOOK_TEST_ON_MOCK_CALL__ === 'function'
) {
const onMockCall = global.__STORYBOOK_TEST_ON_MOCK_CALL__ as typeof onMockCallType;
onMockCall((mock, args) => action(mock.getMockName())(args));
subscribed = true;
}
};

export const loaders: LoaderFunction[] = [attachActionsToFunctionMocks];
export const loaders: LoaderFunction[] = [logActionsWhenMockCalled];
24 changes: 24 additions & 0 deletions code/addons/actions/template/stories/spies.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { global as globalThis } from '@storybook/global';
import { spyOn } from '@storybook/test';

export default {
component: globalThis.Components.Button,
loaders() {
spyOn(console, 'log').mockName('console.log');
},
args: {
label: 'Button',
},
parameters: {
chromatic: { disable: true },
},
};

export const ShowSpyOnInActions = {
args: {
onClick: () => {
console.log('first');
console.log('second');
},
},
};
42 changes: 0 additions & 42 deletions code/addons/interactions/src/preview.test.ts

This file was deleted.

53 changes: 1 addition & 52 deletions code/addons/interactions/src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import type {
ArgsEnhancer,
PlayFunction,
PlayFunctionContext,
Renderer,
StepLabel,
} from '@storybook/types';
import { fn, isMockFunction } from '@storybook/test';
import type { PlayFunction, PlayFunctionContext, StepLabel } from '@storybook/types';
import { instrument } from '@storybook/instrumenter';

export const { step: runStep } = instrument(
Expand All @@ -16,50 +9,6 @@ export const { step: runStep } = instrument(
{ intercept: true }
);

export const traverseArgs = (value: unknown, depth = 0, key?: string): unknown => {
// Make sure to not get in infinite loops with self referencing args
if (depth > 5) return value;
if (value == null) return value;
if (isMockFunction(value)) {
// Makes sure we get the arg name in the interactions panel
if (key) value.mockName(key);
return value;
}

// wrap explicit actions in a spy
if (
typeof value === 'function' &&
'isAction' in value &&
value.isAction &&
!('implicit' in value && value.implicit)
) {
const mock = fn(value as any);
if (key) mock.mockName(key);
return mock;
}

if (Array.isArray(value)) {
depth++;
return value.map((item) => traverseArgs(item, depth));
}

if (typeof value === 'object' && value.constructor === Object) {
depth++;
for (const [k, v] of Object.entries(value)) {
if (Object.getOwnPropertyDescriptor(value, k).writable) {
// We have to mutate the original object for this to survive HMR.
(value as Record<string, unknown>)[k] = traverseArgs(v, depth, k);
}
}
return value;
}
return value;
};

const wrapActionsInSpyFns: ArgsEnhancer<Renderer> = ({ initialArgs }) => traverseArgs(initialArgs);

export const argsEnhancers = [wrapActionsInSpyFns];

export const parameters = {
throwPlayFunctionExceptions: false,
};
2 changes: 1 addition & 1 deletion code/builders/builder-webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"util": "^0.12.4",
"util-deprecate": "^1.0.2",
"webpack": "5",
"webpack-dev-middleware": "^6.1.1",
"webpack-dev-middleware": "^6.1.2",
"webpack-hot-middleware": "^2.25.1",
"webpack-virtual-modules": "^0.5.0"
},
Expand Down
22 changes: 22 additions & 0 deletions code/e2e-tests/addon-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,26 @@ test.describe('addon-actions', () => {
});
await expect(logItem).toBeVisible();
});

test('should show spies', async ({ page }) => {
test.skip(
templateName.includes('svelte') && templateName.includes('prerelease'),
'Svelte 5 prerelase does not support automatic actions with our current example components yet'
);
await page.goto(storybookUrl);
const sbPage = new SbPage(page);
sbPage.waitUntilLoaded();

await sbPage.navigateToStory('addons/actions/spies', 'show-spy-on-in-actions');

const root = sbPage.previewRoot();
const button = root.locator('button', { hasText: 'Button' });
await button.click();

await sbPage.viewAddonPanel('Actions');
const logItem = await page.locator('#storybook-panel-root #panel-tab-content', {
hasText: 'console.log',
});
await expect(logItem).toBeVisible();
});
});
2 changes: 2 additions & 0 deletions code/lib/cli/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { removeJestTestingLibrary } from './remove-jest-testing-library';
import { addonsAPI } from './addons-api';
import { mdx1to3 } from './mdx-1-to-3';
import { addonPostCSS } from './addon-postcss';
import { vta } from './vta';
import { upgradeStorybookRelatedDependencies } from './upgrade-storybook-related-dependencies';

export * from '../types';
Expand Down Expand Up @@ -58,6 +59,7 @@ export const allFixes: Fix[] = [
webpack5CompilerSetup,
mdx1to3,
upgradeStorybookRelatedDependencies,
vta,
];

export const initFixes: Fix[] = [eslintPlugin];
61 changes: 61 additions & 0 deletions code/lib/cli/src/automigrate/fixes/vta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import type { StorybookConfig } from '@storybook/types';
import { vta } from './vta';

const check = async ({
packageManager,
main: mainConfig,
storybookVersion = '7.0.0',
}: {
packageManager: any;
main: Partial<StorybookConfig> & Record<string, unknown>;
storybookVersion?: string;
}) => {
return vta.check({
packageManager,
configDir: '',
mainConfig: mainConfig as any,
storybookVersion,
});
};

describe('no-ops', () => {
it('with addon setup', async () => {
await expect(
check({
packageManager: {},
main: {
addons: ['@chromatic-com/storybook'],
},
})
).resolves.toBeFalsy();
});
it('with addon setup with options', async () => {
await expect(
check({
packageManager: {},
main: {
addons: [
{
name: '@chromatic-com/storybook',
options: {},
},
],
},
})
).resolves.toBeFalsy();
});
});

describe('continue', () => {
it('no addons', async () => {
await expect(
check({
packageManager: {},
main: {
stories: ['**/*.stories.mdx'],
},
})
).resolves.toBeTruthy();
});
});
55 changes: 55 additions & 0 deletions code/lib/cli/src/automigrate/fixes/vta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { dedent } from 'ts-dedent';
import chalk from 'chalk';
import { getAddonNames, updateMainConfig } from '../helpers/mainConfigFile';
import type { Fix } from '../types';

const logger = console;

interface Options {}

/**
*/
export const vta: Fix<Options> = {
id: 'visual-tests-addon',

versionRange: ['<8.0.7', '>=8.0.7'],

async check({ mainConfig }) {
const hadAddonInstalled = getAddonNames(mainConfig).some((addon) =>
addon.includes('@chromatic-com/storybook')
);

const skip = hadAddonInstalled;

if (skip) {
return null;
}

return {};
},

prompt() {
return dedent`
New to Storybook 8: Storybook's Visual Tests addon helps you catch unintentional changes/bugs in your stories. The addon is powered by Chromatic, a cloud-based testing tool developed by Storybook's core team.
Learn more: ${chalk.yellow('storybook.js.org/docs/writing-tests/visual-testing')}
Install Visual Tests addon in your project?
`;
},

async run({ packageManager, dryRun, mainConfigPath, skipInstall }) {
if (!dryRun) {
const packageJson = await packageManager.retrievePackageJson();
await packageManager.addDependencies(
{ installAsDevDependencies: true, skipInstall, packageJson },
[`@chromatic-com/storybook@^1`]
);

await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => {
logger.info(`✅ Adding "@chromatic-com/storybook" addon`);
main.appendValueToArray(['addons'], '@chromatic-dom/storybook');
});
}
},
};
1 change: 1 addition & 0 deletions code/lib/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"util": "^0.12.4"
},
"devDependencies": {
"tinyspy": "^2.2.0",
"ts-dedent": "^2.2.0",
"type-fest": "~2.19",
"typescript": "^5.3.2"
Expand Down

0 comments on commit 799416e

Please sign in to comment.