Skip to content

Commit

Permalink
Enable cypress for v8 (#21678)
Browse files Browse the repository at this point in the history
  • Loading branch information
ecraig12345 authored Feb 12, 2022
1 parent 21ad43b commit 525ac96
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 59 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
// Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the file.exclude setting.
"search.exclude": {
".yarn/releases": true,
"common/temp": true,
"**/node_modules": true,
"**/lib": true,
Expand Down
4 changes: 2 additions & 2 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ jobs:
# This MUST have a trailing slash, or the links to PR deploy site assets won't work
githubTargetLink: $(deployUrl)/

# only run e2e tests when converged storybook is published by scoping to the converged suite package
# only run e2e tests when the appropriate storybook is published by scoping to relevant packages
- script: |
yarn e2e $(sinceArg) --scope @fluentui/react-components
yarn e2e $(sinceArg) --scope @fluentui/react-components --scope @fluentui/react
displayName: Cypress E2E tests
- template: .devops/templates/cleanup.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add ./e2e to list of files allowing devDependencies",
"packageName": "@fluentui/eslint-plugin",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Add command to run e2e tests",
"packageName": "@fluentui/react",
"email": "[email protected]",
"dependentChangeType": "none"
}
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/utils/configHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const testFiles = [
'**/{test,tests}/**',
'**/testUtilities.{ts,tsx}',
'**/common/{isConformant,snapshotSerializers}.{ts,tsx}',
'./e2e/**',
];

const docsFiles = ['**/*Page.tsx', '**/{docs,demo}/**', '**/*.doc.{ts,tsx}'];
Expand Down
79 changes: 29 additions & 50 deletions packages/react-examples/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { withPerformance } from 'storybook-addon-performance';
import { withKeytipLayer, withStrictMode } from '@fluentui/storybook';

/**
* "PACKAGE_NAME" placeholder is being replaced by webpack loader - @link {./preview.loader}
* This placeholder will be replaced with the actual package name by custom webpack loader `./preview-loader.js`
* @type {string}
*/
const packageNamePlaceholder = 'PACKAGE_NAME';
Expand Down Expand Up @@ -36,16 +36,18 @@ export const parameters = {};
* default: { title: string };
* [subStoryName: string]: React.FunctionComponent | { title: string };
* }} Story
*/

/**
*
* @typedef {{ [exportName: string]: React.ComponentType }} ComponentModule
*/

/** */
function loadStories() {
/** @type {Map<string, Story>} */
const stories = new Map();

// This shows some extra e2e-only stories
const includeE2E = new URLSearchParams(location.search).has('e2e');

/** @type {__WebpackModuleApi.RequireContext[]} */
const contexts = [
// This will be updated by preview-loader with the actual current package name
Expand All @@ -62,7 +64,9 @@ function loadStories() {

for (const req of contexts) {
req.keys().forEach(key => {
generateStoriesFromExamples(key, stories, req);
if (includeE2E || !key.includes('/e2e/')) {
generateStoriesFromExamples(key, stories, req);
}
});
}

Expand All @@ -81,71 +85,46 @@ function loadStories() {
function generateStoriesFromExamples(key, stories, req) {
// Depending on the starting point of the context, and the package layout, the key will be like one of these:
// ./ComponentName/ComponentName.Something.Example.tsx
// ./ComponentName/e2e/ComponentName.Something.stories.tsx
// ./package-name/ComponentName/ComponentName.Something.Example.tsx
// ./package-name/src/.../ComponentName.stories.tsx - @TODO remove this line after new storybook setup has been applied for all converged packages
const segments = key.split('/');

if (segments.length < 3) {
console.warn(`Invalid storybook context location found: key: ${key} | segments: ${segments}`);
// ./package-name/ComponentName/e2e/ComponentName.Something.Example.tsx
// group 1: component name
// group 2: /e2e part (if present)
// group 3: story name
const pathParts = key.match(/\/(\w+)(\/e2e)?\/[\w.]+$/);
if (!pathParts) {
console.error(`Invalid path found in storybook require.context: "${key}"`);
return;
}

const componentName = generateComponentName(segments);
const [, componentName, e2e = ''] = pathParts;
// This will be like either:
// Components/ComponentName
// Components/ComponentName/e2e
const componentPath = `Components/${componentName}${e2e}`;

if (!stories.has(componentName)) {
stories.set(componentName, {
if (!stories.has(componentPath)) {
stories.set(componentPath, {
default: {
title: 'Components/' + componentName,
title: componentPath,
},
});
}

const storyName = segments.slice(-1)[0].replace('.tsx', '').replace(/\./g, '_');

const story = stories.get(componentName);
const story = /** @type {Story} */ (stories.get(componentPath));
const exampleModule = /** @type {(key: string) => ComponentModule} */ (req)(key);

if (!story) {
console.warn(`No stories for component: ${componentName}`);
return;
}

for (let moduleExport of Object.keys(exampleModule)) {
const ExampleComponent = exampleModule[moduleExport];
const subStoryName = moduleExport || storyName;
for (const [moduleExport, ExampleComponent] of Object.entries(exampleModule)) {
const subStoryName = moduleExport.replace(componentName, '').replace(/Example$/, '');

if (typeof ExampleComponent === 'function') {
if (ExampleComponent.prototype.render) {
// class component
// class component -- make a wrapper function component
story[subStoryName] = () => React.createElement(ExampleComponent);
} else {
// function component
story[subStoryName] = /** @type {React.FunctionComponent} */ (ExampleComponent);
}
}
}

/**
*
* @param {string[]} segments
* @returns {string} component name
*/
function generateComponentName(segments) {
/**
* ./ComponentName/ComponentName.Something.Example.tsx
*/
const isReactExamplesStory = segments.length === 3;

if (isReactExamplesStory) {
// ./ComponentName/ComponentName.Something.Example.tsx
// ↓↓↓
// [., ComponentName, ComponentName.Something.Example.tsx]
return segments[1];
}

// .package-name/ComponentName/ComponentName.Something.Example.tsx
// ↓↓↓
// [., package-name, ComponentName, ComponentName.Something.Example.tsx]
return segments[2];
}
}
2 changes: 2 additions & 0 deletions packages/react-examples/e2e/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// workaround for https://github.com/cypress-io/cypress/issues/8599
import '@fluentui/scripts/cypress/support';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { FocusZone, FocusTrapZone, FocusZoneDirection, mergeStyles } from '@fluentui/react';

const rootClass = mergeStyles({
position: 'relative',
button: { position: 'absolute', height: 30, width: 30 },
'#a': { top: 0, left: 0 },
'#b': { top: 0, left: 30 },
'#c': { top: 0, left: 60 },
'#d': { top: 30, left: 0 },
'#e': { top: 30, left: 30 },
'#f': { top: 30, left: 60 },
});

/**
* Tab and shift-tab wrap at extreme ends of the FTZ:
*
* can tab across FocusZones with different button structures
*/
export const TabWrappingMultiFocusZone = () => {
return (
<div className={rootClass}>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusZone direction={FocusZoneDirection.horizontal}>
<div>
<button id="a">a</button>
</div>
<div>
<button id="b">b</button>
</div>
<div>
<button id="c">c</button>
</div>
</FocusZone>
<FocusZone direction={FocusZoneDirection.horizontal}>
<div>
<div>
<button id="d">d</button>
<button id="e">e</button>
<button id="f">f</button>
</div>
</div>
</FocusZone>
</FocusTrapZone>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const ftzStoriesTitle = 'Components/FocusTrapZone/e2e';

describe('FocusTrapZone', () => {
before(() => {
cy.visitStorybook({ qs: { e2e: '1' } });
});

describe('Tab and shift-tab wrap at extreme ends of the FTZ', () => {
it('can tab across FocusZones with different button structures', () => {
cy.loadStory(ftzStoriesTitle, 'TabWrappingMultiFocusZone');

cy.get('#a').focus();
cy.focused().should('have.id', 'a');

// shift+tab to focus first bumper
cy.realPress(['Shift', 'Tab']);
cy.focused().should('have.id', 'd');

// tab to focus last bumper
cy.realPress('Tab');
cy.focused().should('have.id', 'a');
});
});
});
2 changes: 1 addition & 1 deletion packages/react-examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"skipLibCheck": true,
"lib": ["es5", "dom", "es2015.promise"],
"typeRoots": ["../../node_modules/@types", "../../typings"],
"types": ["webpack-env", "custom-global", "node"],
"types": ["webpack-env", "custom-global", "node", "cypress", "cypress-storybook/cypress", "cypress-real-events"],
"paths": {
"@fluentui/react-examples/lib/*": ["./src/*"],
"@fluentui/react-examples": ["./src"]
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"clean": "just-scripts clean",
"code-style": "just-scripts code-style",
"codepen": "node ../../scripts/local-codepen.js",
"e2e": "yarn workspace @fluentui/react-examples e2e --package react",
"just": "just-scripts",
"lint": "just-scripts lint",
"start": "cross-env NODE_OPTIONS=--max-old-space-size=3072 just-scripts dev:storybook",
Expand Down
21 changes: 15 additions & 6 deletions scripts/cypress.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,36 @@ const cypress = require('cypress');
const path = require('path');

/**
* Script that run/opens cypress, since cypress does not support easy config extension
* Can be removed in favour of native CLI once cypress supports path based config extension
* https://github.com/cypress-io/cypress/issues/5674
* Script that run/opens cypress, since cypress does not support easy config extension.
* Can be removed in favour of native CLI once cypress supports path-based config extension.
* https://github.com/cypress-io/cypress/issues/5218
*
* To debug cypress tests locally, run the following in your package folder in *separate terminals*:
* - `yarn start` and make a note of the port
* - `yarn e2e --mode open --port ####`
*/

const argv = require('yargs')
.option('mode', {
describe: 'Choose a mode to run cypress',
choices: ['run', 'open'],
})
.option('package', {
describe: 'Unscoped package name to load the deployed storybook for (used by PR runs only)',
default: 'react-components',
type: 'option',
choices: ['react-components', 'react'],
})
.option('port', {
describe: 'Port number storybook is running on',
describe: 'Port number storybook is running on (used by local runs only)',
default: 3000,
type: 'number',
})
.demandOption('mode').argv;

const baseConfig = {
baseUrl: process.env.DEPLOYURL
? // Base path hard coded for converged for now, can be modified to be configurable if required to other projects
`${process.env.DEPLOYURL}/react-components/storybook`
? `${process.env.DEPLOYURL}/${argv.package}/storybook`
: `http://localhost:${argv.port}`,
fixturesFolder: path.join(__dirname, 'cypress/fixtures'),
integrationFolder: '.',
Expand Down

0 comments on commit 525ac96

Please sign in to comment.