Skip to content

Commit 9aa2372

Browse files
authored
Add Playwright tests (#153)
1 parent 9c949b0 commit 9aa2372

File tree

12 files changed

+501
-3
lines changed

12 files changed

+501
-3
lines changed

.github/workflows/buildAndTest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/setup-node@v4
1515
with:
1616
cache: yarn
17-
- name: Install packages
17+
- name: Install dependencies
1818
run: yarn install
1919
- name: Format
2020
run: yarn run format:check

.github/workflows/playwright.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Playwright Tests
2+
on:
3+
push:
4+
branches: [master]
5+
pull_request:
6+
branches: [master]
7+
jobs:
8+
test:
9+
timeout-minutes: 60
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
cache: yarn
16+
node-version: lts/*
17+
- name: Install dependencies
18+
run: yarn install
19+
- name: Install Playwright Browsers
20+
run: yarn playwright install --with-deps
21+
- name: Set environment variable
22+
run: echo "GH_ACCESS_TOKEN=${{ secrets.GH_ACCESS_TOKEN }}" >> $GITHUB_ENV
23+
- name: Run Playwright tests
24+
run: yarn playwright test
25+
- uses: actions/upload-artifact@v4
26+
if: ${{ !cancelled() }}
27+
with:
28+
name: playwright-report
29+
path: playwright-report/
30+
retention-days: 30

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,13 @@ npm-debug.log*
2222
yarn-debug.log*
2323
yarn-error.log*
2424

25+
node_modules/
26+
2527
# vscode
2628
.vscode
29+
30+
# Playwright
31+
/test-results/
32+
/playwright-report/
33+
/blob-report/
34+
/playwright/.cache/

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<div align="center">
22
<img src="public/logo.png" width="550" />
33

4-
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/seladb/StarTrack-js/buildAndTest.yml?branch=typescript&label=Actions&logo=github&style=flat)](https://github.com/seladb/StarTrack-js/actions?query=workflow:%22Build%20and%20Test%22)
4+
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/seladb/StarTrack-js/buildAndTest.yml?label=Actions&logo=github&style=flat)](https://github.com/seladb/StarTrack-js/actions?query=workflow:%22Build%20and%20Test%22)
5+
[![Playwright Tests Status](https://img.shields.io/github/actions/workflow/status/seladb/StarTrack-js/playwright.yml?label=Playwright&logo=github&style=flat)](https://github.com/seladb/StarTrack-js/actions?query=workflow:%22Playwright%20Tests%22)
56
[![Coverage Status](https://coveralls.io/repos/github/seladb/StarTrack-js/badge.svg?branch=master)](https://coveralls.io/github/seladb/StarTrack-js?branch=master)
67

78
</div>
@@ -104,6 +105,7 @@ Dev packages:
104105

105106
- [ESLint](https://eslint.org/), [Prettier](https://prettier.io/) and their plugins for code linting and formatting
106107
- [CSpell](https://cspell.org/) for spell check
108+
- [Playwright](https://playwright.dev/) for integration tests
107109

108110
To run it locally follow these steps:
109111

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,13 @@
7575
]
7676
},
7777
"devDependencies": {
78+
"@playwright/test": "^1.49.0",
7879
"@types/css-mediaquery": "^0.1.4",
7980
"@types/react-plotly.js": "^2.6.3",
8081
"@typescript-eslint/eslint-plugin": "^6.20.0",
8182
"@typescript-eslint/parser": "^7.16.1",
8283
"cspell": "^8.15.5",
84+
"csv-parse": "^5.6.0",
8385
"eslint": "^8.57.0",
8486
"eslint-config-prettier": "^9.1.0",
8587
"eslint-plugin-prettier": "^5.2.1",

playwright.config.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// cspell: ignore viewports
2+
3+
import { defineConfig, devices } from "@playwright/test";
4+
5+
/**
6+
* Read environment variables from file.
7+
* https://github.com/motdotla/dotenv
8+
*/
9+
// import dotenv from 'dotenv';
10+
// import path from 'path';
11+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
12+
13+
/**
14+
* See https://playwright.dev/docs/test-configuration.
15+
*/
16+
export default defineConfig({
17+
testDir: "./tests",
18+
/* Run tests in files in parallel */
19+
fullyParallel: true,
20+
/* Fail the build on CI if you accidentally left test.only in the source code. */
21+
forbidOnly: !!process.env.CI,
22+
/* Retry on CI only */
23+
retries: process.env.CI ? 2 : 0,
24+
/* Opt out of parallel tests on CI. */
25+
workers: process.env.CI ? 1 : undefined,
26+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
27+
reporter: "html",
28+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29+
use: {
30+
/* Base URL to use in actions like `await page.goto('/')`. */
31+
baseURL: "http://127.0.0.1:3000",
32+
33+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
34+
trace: "on-first-retry",
35+
},
36+
37+
/* Configure projects for major browsers */
38+
projects: [
39+
{
40+
name: "chromium",
41+
use: { ...devices["Desktop Chrome"] },
42+
},
43+
44+
{
45+
name: "firefox",
46+
use: { ...devices["Desktop Firefox"] },
47+
},
48+
49+
{
50+
name: "webkit",
51+
use: { ...devices["Desktop Safari"] },
52+
},
53+
54+
/* Test against mobile viewports. */
55+
// {
56+
// name: 'Mobile Chrome',
57+
// use: { ...devices['Pixel 5'] },
58+
// },
59+
// {
60+
// name: 'Mobile Safari',
61+
// use: { ...devices['iPhone 12'] },
62+
// },
63+
64+
/* Test against branded browsers. */
65+
// {
66+
// name: 'Microsoft Edge',
67+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
68+
// },
69+
// {
70+
// name: 'Google Chrome',
71+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
72+
// },
73+
],
74+
75+
/* Run your local dev server before starting the tests */
76+
webServer: {
77+
command: "yarn run start",
78+
url: "http://127.0.0.1:3000",
79+
reuseExistingServer: !process.env.CI,
80+
},
81+
});

tests/auth.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { test, expect } from "@playwright/test";
2+
import { localUrl } from "./utils";
3+
4+
test("GitHub Authentication", async ({ page }) => {
5+
expect(process.env.GH_ACCESS_TOKEN).toBeDefined();
6+
7+
await page.goto(localUrl);
8+
await page.getByRole("button", { name: "GitHub Authentication" }).click();
9+
await page.getByLabel("GitHub Access Token *").fill(process.env.GH_ACCESS_TOKEN);
10+
await page.getByRole("button", { name: "Login" }).click();
11+
12+
await page.getByRole("button", { name: process.env.GH_ACCESS_TOKEN.slice(-6) }).click();
13+
expect(page.getByRole("menuitem", { name: "Stored in session storage" })).toBeVisible();
14+
15+
expect(await page.evaluate(() => sessionStorage)).toHaveProperty(
16+
"startrack_js_access_token",
17+
process.env.GH_ACCESS_TOKEN,
18+
);
19+
20+
// Log out
21+
await page.getByRole("menuitem", { name: "Log out" }).click();
22+
expect(await page.evaluate(() => sessionStorage)["startrack_js_access_token"]).toBeUndefined();
23+
const gitHubAuthButton = page.getByRole("button", { name: "GitHub Authentication" });
24+
await gitHubAuthButton.waitFor({ state: "visible", timeout: 5000 });
25+
expect(gitHubAuthButton).toBeVisible();
26+
});
27+
28+
test("GitHub Authentication Local Storage", async ({ page }) => {
29+
expect(process.env.GH_ACCESS_TOKEN).toBeDefined();
30+
31+
await page.goto(localUrl);
32+
await page.getByRole("button", { name: "GitHub Authentication" }).click();
33+
await page.getByLabel("GitHub Access Token *").fill(process.env.GH_ACCESS_TOKEN);
34+
await page.getByText("Save access token in local storage").click();
35+
await page.getByRole("button", { name: "Login" }).click();
36+
37+
await page.getByRole("button", { name: process.env.GH_ACCESS_TOKEN.slice(-6) }).click();
38+
expect(page.getByRole("menuitem", { name: "Stored in local storage" })).toBeVisible();
39+
40+
expect(await page.evaluate(() => localStorage)).toHaveProperty(
41+
"startrack_js_access_token",
42+
process.env.GH_ACCESS_TOKEN,
43+
);
44+
});

tests/basic.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// cspell: ignore nsewdrag
2+
3+
import { test, expect } from "@playwright/test";
4+
import { getComparator } from "playwright-core/lib/utils";
5+
import {
6+
localUrl,
7+
referenceUrl,
8+
username,
9+
repo1,
10+
repo2,
11+
authenticate,
12+
getChartScreenshot,
13+
} from "./utils";
14+
15+
test("Basic Flow", async ({ page }, testDir) => {
16+
const getGridStarsCount = async (rowNum: number) => {
17+
return Number(
18+
await page.getByRole("row").nth(rowNum).getByRole("gridcell").nth(1).textContent(),
19+
);
20+
};
21+
22+
const takeReferenceScreenshots = async () => {
23+
await page.goto(referenceUrl);
24+
25+
await authenticate(page);
26+
27+
const screenshots: Buffer[] = [];
28+
29+
// Get screenshot for repo1
30+
await page.getByPlaceholder("Username").fill(username);
31+
await page.getByPlaceholder("Repo name").fill(repo1);
32+
await page.getByRole("button", { name: "Go!" }).click();
33+
await page.getByRole("button", { name: "Go!" }).waitFor({ state: "visible" });
34+
35+
screenshots.push(await getChartScreenshot(page, testDir.outputPath("repo1-reference.png")));
36+
37+
// Get screenshot for repo1 + repo2
38+
await page.getByPlaceholder("Username").fill(username);
39+
await page.getByPlaceholder("Repo name").fill(repo2);
40+
await page.getByRole("button", { name: "Go!" }).click();
41+
await page.getByRole("button", { name: "Go!" }).waitFor({ state: "visible" });
42+
43+
screenshots.push(await getChartScreenshot(page, testDir.outputPath("repo1and2-reference.png")));
44+
45+
return screenshots;
46+
};
47+
48+
const expectedScreenshots = await takeReferenceScreenshots();
49+
50+
await page.goto(localUrl);
51+
52+
await authenticate(page);
53+
54+
const screenshots: Buffer[] = [];
55+
56+
// Get stats for repo1
57+
await page.getByPlaceholder("Username").fill(username);
58+
await page.getByPlaceholder("Repo name").fill(repo1);
59+
await page.getByRole("button", { name: "Go!" }).click();
60+
await expect(page.getByRole("button", { name: `${username} / ${repo1}` }).first()).toBeVisible();
61+
await expect(
62+
page.getByRole("gridcell", { name: `${username} / ${repo1}` }).getByRole("button"),
63+
).toBeVisible();
64+
await expect(page.getByRole("textbox").last()).toHaveValue(
65+
`${localUrl}preload?r=${username},${repo1}`,
66+
);
67+
screenshots.push(await getChartScreenshot(page, testDir.outputPath("repo1-actual.png")));
68+
69+
// Get stats for repo2
70+
await page.getByPlaceholder("Username").fill(username);
71+
await page.getByPlaceholder("Repo name").fill(repo2);
72+
await page.getByRole("button", { name: "Go!" }).click();
73+
await expect(page.getByRole("button", { name: `${username} / ${repo1}` }).first()).toBeVisible();
74+
await expect(page.getByRole("button", { name: `${username} / ${repo2}` }).first()).toBeVisible();
75+
await expect(
76+
page.getByRole("gridcell", { name: `${username} / ${repo1}` }).getByRole("button"),
77+
).toBeVisible();
78+
page.getByRole("row").first().first();
79+
await expect(
80+
page.getByRole("gridcell", { name: `${username} / ${repo2}` }).getByRole("button"),
81+
).toBeVisible();
82+
await expect(page.getByRole("textbox").last()).toHaveValue(
83+
`${localUrl}preload?r=${username},${repo1}&r=${username},${repo2}`,
84+
);
85+
screenshots.push(await getChartScreenshot(page, testDir.outputPath("repo1and2-actual.png")));
86+
87+
const comparator = getComparator("image/png");
88+
screenshots.forEach((screenshot, index) => {
89+
expect(comparator(expectedScreenshots[index], screenshot)).toBeNull();
90+
});
91+
92+
const gridStarsCountRepo1 = await getGridStarsCount(1);
93+
const gridStarsCountRepo2 = await getGridStarsCount(2);
94+
95+
// Sync stats to chart data
96+
await page.getByLabel("Sync stats to chart zoom level").check();
97+
await expect(page.getByText("Date range")).not.toBeVisible();
98+
await page.locator(".nsewdrag").scrollIntoViewIfNeeded();
99+
const chartBoundingBox = await page.locator(".nsewdrag").boundingBox();
100+
expect(chartBoundingBox).not.toBeNull();
101+
if (chartBoundingBox) {
102+
await page.mouse.move(
103+
chartBoundingBox?.x + chartBoundingBox?.width / 2,
104+
chartBoundingBox?.y + chartBoundingBox?.height / 2,
105+
);
106+
await page.mouse.down();
107+
await page.mouse.move(
108+
chartBoundingBox?.x + chartBoundingBox?.width / 2 + 150,
109+
chartBoundingBox?.y + chartBoundingBox?.height / 2,
110+
);
111+
await page.mouse.up();
112+
}
113+
await expect(page.getByText("Date range")).toBeVisible();
114+
expect(await getGridStarsCount(1)).toBeLessThan(gridStarsCountRepo1);
115+
expect(await getGridStarsCount(2)).toBeLessThan(gridStarsCountRepo2);
116+
117+
// Remove repo2
118+
await page.getByTestId("CancelIcon").last().click();
119+
const screenshotWithRepo1 = await getChartScreenshot(
120+
page,
121+
testDir.outputPath("repo1b-actual.png"),
122+
);
123+
expect(comparator(screenshots[0], screenshotWithRepo1)).toBeNull();
124+
125+
// Remove repo1
126+
await page.getByTestId("CancelIcon").last().click();
127+
const chartElement = page.locator(".nsewdrag");
128+
const gridElement = page.getByRole("gridcell");
129+
const downloadDataElement = page.getByText("Download Data");
130+
await chartElement.waitFor({ state: "hidden", timeout: 5000 });
131+
await gridElement.waitFor({ state: "hidden", timeout: 5000 });
132+
await downloadDataElement.waitFor({ state: "hidden", timeout: 5000 });
133+
expect(chartElement).not.toBeVisible();
134+
expect(gridElement).not.toBeVisible();
135+
expect(downloadDataElement).not.toBeVisible();
136+
});

0 commit comments

Comments
 (0)