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

E2E testing #38

Merged
merged 28 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
abb4d0f
npm i -DE @playwright/test@latest -w docs
satelllte Oct 14, 2023
65d2c5c
test:e2e script draft
satelllte Oct 14, 2023
94f273f
fix: CI file
satelllte Oct 14, 2023
4bab1d1
vitest: exclude "e2e" directory
satelllte Oct 14, 2023
1564788
npm run format
satelllte Oct 14, 2023
2fbe8b6
local server test
satelllte Oct 14, 2023
efcde74
playwright: remove commented code
satelllte Oct 14, 2023
8caa553
add mobile browsers, disable firefox (temporary)
satelllte Oct 14, 2023
44b233a
don't use channel "chrome"
satelllte Oct 14, 2023
bf98b31
rename projects
satelllte Oct 14, 2023
3d30c9d
CI: "Install Playwright Browsers" step
satelllte Oct 14, 2023
7ac3daa
browser: firefox-desktop
satelllte Oct 14, 2023
135b31e
CI: remove "install browsers" step
satelllte Oct 14, 2023
6766007
firefox-desktop: mute
satelllte Oct 14, 2023
dc7ec2b
E2E test
satelllte Oct 15, 2023
8002d74
fix decorative knob aria-label
satelllte Oct 15, 2023
1ff6b63
drag: increase steps count
satelllte Oct 15, 2023
8344642
improve knob drag assertions
satelllte Oct 15, 2023
1ad3353
mouse: move to x, y explicitly before dragging
satelllte Oct 15, 2023
3fd7a09
temporary disable drag tests
satelllte Oct 15, 2023
efd7c06
CI: expectKnobDraggingUp
satelllte Oct 15, 2023
f3e6843
calculate bounds properly
satelllte Oct 15, 2023
c78ce8f
examples: test dragging
satelllte Oct 15, 2023
11fb8ad
fix: hover before calculating bounds
satelllte Oct 15, 2023
74bf38c
remove unused "multiplier" prop on drag expects
satelllte Oct 15, 2023
51be7cc
expects, locators
satelllte Oct 15, 2023
1b201de
increase drag amplitude for CI tests
satelllte Oct 15, 2023
2b9edf7
expects.sourceCodeLinkIsValid
satelllte Oct 15, 2023
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
26 changes: 24 additions & 2 deletions .github/workflows/ci.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: Test

on:
pull_request:
Expand All @@ -9,7 +9,7 @@ on:
- main

jobs:
ci:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand All @@ -30,3 +30,25 @@ jobs:
run: npm run test:format
- name: Build package
run: npm run build:package
test-e2e:
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright:v1.39.0-jammy
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Test (E2E)
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-docs
path: apps/docs/playwright-report/
retention-days: 30
4 changes: 4 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Playwright
/test-results/
/playwright-report/
/playwright/.cache/
118 changes: 118 additions & 0 deletions apps/docs/e2e/expects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {expect, type Page, type Locator} from '@playwright/test';

const _dragSteps = 10;
const _dragAmplitude = 40.0;

const _calculateElementCenter = async (element: Locator) => {
const bounds = await element.evaluate((el) => el.getBoundingClientRect());
const x = bounds.x + bounds.width / 2;
const y = bounds.y + bounds.height / 2;
return {x, y};
};

export const knobValueTextIs = async (
knob: Locator,
{knobOutput, valueText}: {knobOutput?: Locator; valueText: string},
) => {
expect(await knob.getAttribute('aria-valuetext')).toBe(valueText);
if (knobOutput) {
await expect(knobOutput).toHaveText(valueText);
}
};

export const knobValueIsEqualTo = async (
knob: Locator,
{valueNow}: {valueNow: number},
) => {
expect(await knob.getAttribute('aria-valuenow')).toBe(`${valueNow}`);
};

export const knobValueIsLessThan = async (
knob: Locator,
{value}: {value: number},
) => {
expect(Number(await knob.getAttribute('aria-valuenow'))).toBeLessThan(value);
};

export const knobValueIsMoreThan = async (
knob: Locator,
{value}: {value: number},
) => {
expect(Number(await knob.getAttribute('aria-valuenow'))).toBeGreaterThan(
value,
);
};

export const knobDragsUpCorrectly = async (
knob: Locator,
{
valueNow,
page,
}: {
valueNow: number;
page: Page;
},
) => {
// It's necessary to hover over the knob and scroll it into view,
// so then we can calculate its bounds in the viewport properly
await knob.hover();

const {x, y} = await _calculateElementCenter(knob);

await page.mouse.down();
await page.mouse.move(x, y - _dragAmplitude, {steps: _dragSteps});
await page.mouse.up();
await knobValueIsMoreThan(knob, {value: valueNow});
};

export const knobDragsDownCorrectly = async (
knob: Locator,
{
valueNow,
page,
}: {
valueNow: number;
page: Page;
},
) => {
// It's necessary to hover over the knob and scroll it into view,
// so then we can calculate its bounds in the viewport properly
await knob.hover();

const {x, y} = await _calculateElementCenter(knob);

await page.mouse.down();
await page.mouse.move(x, y + _dragAmplitude, {steps: _dragSteps});
await page.mouse.up();
await knobValueIsLessThan(knob, {value: valueNow});
};

export const sourceCodeLinkIsValid = async ({
link,
page,
filePath,
}: {
link: Locator;
page: Page;
filePath: string;
}) => {
const url = `https://github.com/satelllte/react-knob-headless/blob/main/${filePath}`;
await expect(link).toHaveAttribute('href', url);
await link.click();

const githubPage = await page.waitForEvent('popup');
await expect(githubPage).toHaveURL(url);
await expect(githubPage).toHaveTitle(
`react-knob-headless/${filePath} at main · satelllte/react-knob-headless · GitHub`,
);
};

export const expects = {
knobValueTextIs,
knobValueIsEqualTo,
knobValueIsLessThan,
knobValueIsMoreThan,
knobDragsUpCorrectly,
knobDragsDownCorrectly,
sourceCodeLinkIsValid,
} as const;
141 changes: 141 additions & 0 deletions apps/docs/e2e/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {test, expect, type Locator} from '@playwright/test';
import {locators} from './locators';
import {expects} from './expects';

test.beforeEach(async ({page}) => {
await page.goto('/');
});

test.describe('Page title', () => {
test('is correct', async ({page}) => {
await expect(page).toHaveTitle('React Knob Headless');
});
});

test.describe('Repository link', () => {
test('has correct reference', async ({page}) => {
const repositoryLink = locators.repositoryLink(page);
await expect(repositoryLink).toBeVisible();
await expect(repositoryLink).toHaveAttribute(
'href',
'https://github.com/satelllte/react-knob-headless',
);
});

test('opens GitHub repository page', async ({page}) => {
const repositoryLink = locators.repositoryLink(page);
await repositoryLink.click();

const repositoryPage = await page.waitForEvent('popup');
await expect(repositoryPage).toHaveTitle(
'GitHub - satelllte/react-knob-headless: 🎛️ Unstyled & accessible knob primitive for React.',
);
});
});

test.describe('Decorative knobs', () => {
test('have correct default values and drag behaviour', async ({page}) => {
const knobStone = locators.decorativeKnobStone(page);
const knobPink = locators.decorativeKnobPink(page);
const knobGreen = locators.decorativeKnobGreen(page);
const knobSky = locators.decorativeKnobSky(page);

await expects.knobValueIsEqualTo(knobStone, {valueNow: 0});
await expects.knobValueTextIs(knobStone, {valueText: '0 units'});
await expects.knobDragsUpCorrectly(knobStone, {valueNow: 0, page});

await expects.knobValueIsEqualTo(knobPink, {valueNow: 40});
await expects.knobValueTextIs(knobPink, {valueText: '40 units'});
await expects.knobDragsDownCorrectly(knobPink, {valueNow: 40, page});

await expects.knobValueIsEqualTo(knobGreen, {valueNow: 80});
await expects.knobValueTextIs(knobGreen, {valueText: '80 units'});
await expects.knobDragsUpCorrectly(knobGreen, {valueNow: 80, page});

await expects.knobValueIsEqualTo(knobSky, {valueNow: 100});
await expects.knobValueTextIs(knobSky, {valueText: '100 units'});
await expects.knobDragsDownCorrectly(knobSky, {valueNow: 100, page});
});
});

test.describe('"Simple linear knob" example', () => {
let container: Locator;

test.beforeEach(({page}) => {
container = locators.exampleContainer(page, 'Simple linear knob');
});

test('has "View source" link leading to "KnobPercentage.tsx" source code file', async ({
page,
}) => {
const viewSourceLink = locators.exampleViewSourceLink(container);
await expects.sourceCodeLinkIsValid({
link: viewSourceLink,
page,
filePath: 'apps/docs/src/components/knobs/KnobPercentage.tsx',
});
});

test.describe('"Dry/Wet" knob', () => {
let knob: Locator;

test.beforeEach(() => {
knob = locators.exampleKnob(container, 'Dry/Wet');
});

test('has correct default value', async () => {
const knobOutput = locators.exampleKnobOutput(container);
await expects.knobValueIsEqualTo(knob, {valueNow: 50});
await expects.knobValueTextIs(knob, {knobOutput, valueText: '50%'});
});

test('has correct drag down behaviour', async ({page}) => {
await expects.knobDragsDownCorrectly(knob, {valueNow: 50, page});
});

test('has correct drag up behaviour', async ({page}) => {
await expects.knobDragsUpCorrectly(knob, {valueNow: 50, page});
});
});
});

test.describe('"Interpolated knob" example', () => {
let container: Locator;

test.beforeEach(({page}) => {
container = locators.exampleContainer(page, 'Interpolated knob');
});

test('has "View source" link leading to "KnobFrequency.tsx" source code file', async ({
page,
}) => {
const viewSourceLink = locators.exampleViewSourceLink(container);
await expects.sourceCodeLinkIsValid({
link: viewSourceLink,
page,
filePath: 'apps/docs/src/components/knobs/KnobFrequency.tsx',
});
});

test.describe('"Frequency" knob', () => {
let knob: Locator;

test.beforeEach(() => {
knob = locators.exampleKnob(container, 'Frequency');
});

test('has correct default value', async () => {
const knobOutput = locators.exampleKnobOutput(container);
await expects.knobValueIsEqualTo(knob, {valueNow: 440});
await expects.knobValueTextIs(knob, {knobOutput, valueText: '440 Hz'});
});

test('has correct drag down behaviour', async ({page}) => {
await expects.knobDragsDownCorrectly(knob, {valueNow: 440, page});
});

test('has correct drag up behaviour', async ({page}) => {
await expects.knobDragsUpCorrectly(knob, {valueNow: 440, page});
});
});
});
49 changes: 49 additions & 0 deletions apps/docs/e2e/locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {type Locator, type Page} from '@playwright/test';

const repositoryLink = (page: Page) =>
page.getByRole('link', {
name: 'GitHub repository satelllte/react-knob-headless',
});

const decorativeKnobStone = (page: Page) =>
page.getByRole('slider', {
name: "Decorative knob with 'stone' theme",
});

const decorativeKnobPink = (page: Page) =>
page.getByRole('slider', {
name: "Decorative knob with 'pink' theme",
});

const decorativeKnobGreen = (page: Page) =>
page.getByRole('slider', {
name: "Decorative knob with 'green' theme",
});

const decorativeKnobSky = (page: Page) =>
page.getByRole('slider', {
name: "Decorative knob with 'sky' theme",
});

const exampleContainer = (page: Page, name: string) =>
page.getByRole('heading', {name}).locator('..');

const exampleViewSourceLink = (container: Locator) =>
container.getByRole('link', {name: 'View source'});

const exampleKnob = (container: Locator, name: string) =>
container.getByRole('slider', {name});

const exampleKnobOutput = (container: Locator) => container.getByRole('status');

export const locators = {
repositoryLink,
decorativeKnobStone,
decorativeKnobPink,
decorativeKnobGreen,
decorativeKnobSky,
exampleContainer,
exampleViewSourceLink,
exampleKnob,
exampleKnobOutput,
} as const;
4 changes: 3 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"build": "next build",
"start": "next start",
"test": "vitest",
"test:lint": "next lint --max-warnings=0 --format=compact"
"test:lint": "next lint --max-warnings=0 --format=compact",
"test:e2e": "playwright test"
},
"dependencies": {
"@radix-ui/react-icons": "1.3.0",
Expand All @@ -17,6 +18,7 @@
"react-knob-headless": "*"
},
"devDependencies": {
"@playwright/test": "1.39.0",
"@types/node": "20.8.6",
"@types/react": "18.2.28",
"@types/react-dom": "18.2.13",
Expand Down
Loading