Skip to content

Commit d198b39

Browse files
committed
WIP feat: init ui tests
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
1 parent be98685 commit d198b39

File tree

11 files changed

+413
-0
lines changed

11 files changed

+413
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# How to test
2+
3+
## How to test the UI
4+
5+
```
6+
playwright test -c ui/tests/e2e
7+
playwright test -c ui/tests/e2e --project=chromium
8+
playwright test -c ui/tests/e2e --project=mobile
9+
```
10+
11+
```
12+
playwright test -c ui/tests/e2e --ui
13+
playwright test -c ui/tests/e2e --ui-host 127.0.0.1
14+
```
15+
16+
```
17+
env BASE_URL="https://ngi-nix.github.io/forge/" playwright test -c ui/tests/e2e --ui-host 127.0.0.1
18+
```

flake/develop/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
python3
2727
self'.packages.elm-watch
2828
self'.packages.elm2nix
29+
playwright-test
2930
systemd-manager-tui
3031
watchman
3132
podman-compose

ui/tests/e2e/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test-results/
2+
playwright-report/

ui/tests/e2e/playwright.config.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
import process from "node:process";
3+
4+
export default defineConfig({
5+
outputDir: "test-results",
6+
reporter: [["html", { open: "never", outputFolder: "playwright-report" }]],
7+
use: {
8+
// NOTE: https://github.com/microsoft/playwright/issues/22592#issuecomment-1519991484
9+
// BASE_URL should end with "/"
10+
// every test should us relative paths prepended with "./"
11+
baseURL: process.env.BASE_URL || "http://localhost:3000",
12+
trace: "retain-on-failure",
13+
video: "retain-on-failure",
14+
colorScheme: "dark", // assume dark by default
15+
},
16+
webServer: {
17+
command: "dev-ui",
18+
url: "http://localhost:3000",
19+
reuseExistingServer: !process.env.CI,
20+
stdout: "pipe",
21+
stderr: "pipe",
22+
},
23+
// Give failing tests 3 retry attempts
24+
retries: 3,
25+
projects: [
26+
{
27+
name: "chromium",
28+
use: {
29+
...devices["Desktop Chrome"],
30+
},
31+
},
32+
{
33+
name: "firefox",
34+
use: {
35+
...devices["Desktop Firefox"],
36+
},
37+
},
38+
{
39+
name: "webkit",
40+
use: {
41+
...devices["Desktop Safari"],
42+
},
43+
},
44+
{
45+
name: "mobile",
46+
use: {
47+
...devices["Pixel 7"],
48+
},
49+
},
50+
],
51+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Run Modal and Hash State", () => {
4+
const targetPage = "./app/python-web";
5+
6+
test("clicking Run opens modal and updates URL to #run", async ({ page }) => {
7+
await page.goto(targetPage);
8+
await page.getByTestId("app-run-button").click();
9+
10+
await expect(page.getByTestId("run-modal-container")).toBeVisible();
11+
expect(page.url()).toContain("#run");
12+
});
13+
14+
test("direct visit to #run opens the modal", async ({ page }) => {
15+
await page.goto(`${targetPage}#run`);
16+
await expect(page.getByTestId("run-modal-container")).toBeVisible();
17+
});
18+
19+
test("direct visit to #run-shell opens modal with Shell tab active", async ({ page }) => {
20+
await page.goto(`${targetPage}#run-shell`);
21+
await expect(page.getByTestId("run-modal-container")).toBeVisible();
22+
23+
const shellTab = page.getByRole("tab", { name: /shell/i });
24+
await expect(shellTab).toHaveClass(/active/);
25+
});
26+
27+
test("modal closes via Escape, close button, and backdrop click", async ({ page }) => {
28+
const modal = page.getByTestId("run-modal-container");
29+
30+
await page.goto(`${targetPage}#run`);
31+
await expect(modal).toBeVisible();
32+
await page.getByTestId("close-modal-button").click();
33+
await expect(modal).toBeHidden();
34+
expect(page.url()).not.toContain("#run");
35+
36+
await page.goto(`${targetPage}#run`);
37+
await expect(modal).toBeVisible();
38+
await page.keyboard.press("Escape");
39+
await expect(modal).toBeHidden();
40+
41+
await page.goto(`${targetPage}#run`);
42+
await expect(modal).toBeVisible();
43+
await page.mouse.click(1, 1);
44+
await expect(modal).toBeHidden();
45+
});
46+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("navigation menu responds to viewport size", async ({ page, isMobile }) => {
4+
await page.goto("./");
5+
6+
const navToggler = page.getByTestId("navbar-toggler");
7+
const optionsLink = page.getByRole("link", { name: /options/i });
8+
9+
if (isMobile) {
10+
await expect(navToggler).toBeVisible();
11+
await expect(optionsLink).not.toBeVisible();
12+
13+
await navToggler.click();
14+
await expect(optionsLink).toBeVisible();
15+
} else {
16+
await expect(navToggler).not.toBeVisible();
17+
await expect(optionsLink).toBeVisible();
18+
}
19+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("theme toggles between dark and light and updates icons", async ({ page, isMobile }) => {
4+
await page.goto("./");
5+
6+
const html = page.locator("html");
7+
8+
if (isMobile) {
9+
const navToggler = page.getByTestId("navbar-toggler");
10+
await navToggler.click();
11+
}
12+
13+
// nth(i) will get the nth match
14+
// for playwright there is a strict mode where getByTestId should only match 1 entry
15+
// but because we have two theme switcher buttons (one always hidden in nav collapse)
16+
// we need to chose the correct selector for mobile and desktop
17+
const index = isMobile ? 1 : 0;
18+
19+
const toggleBtn = page.getByTestId("theme-toggle-btn").nth(index);
20+
const sunIcon = page.getByTestId("icon-sun").nth(index);
21+
const moonIcon = page.getByTestId("icon-moon").nth(index);
22+
23+
// only works becaue we set colorScheme in playwright.config.ts
24+
await expect(html).toHaveAttribute("data-bs-theme", "dark");
25+
await expect(moonIcon).toBeVisible();
26+
await expect(sunIcon).toBeHidden();
27+
28+
await toggleBtn.click();
29+
await expect(html).toHaveAttribute("data-bs-theme", "light");
30+
await expect(sunIcon).toBeVisible();
31+
await expect(moonIcon).toBeHidden();
32+
33+
await toggleBtn.click();
34+
await expect(html).toHaveAttribute("data-bs-theme", "dark");
35+
await expect(moonIcon).toBeVisible();
36+
await expect(sunIcon).toBeHidden();
37+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Permalinks and Smooth Scrolling", () => {
4+
const targetPage = "./app/python-web";
5+
6+
test("direct navigation to a hash anchor scrolls to the element", async ({ page }) => {
7+
await page.goto(`${targetPage}#resources`);
8+
9+
const grantsSection = page.locator("#resources");
10+
11+
await expect(grantsSection).toBeInViewport();
12+
});
13+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Ambient Search in Home page", () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto("./");
6+
});
7+
8+
test("typing anywhere auto-focuses the search bar and captures the key", async ({ page }) => {
9+
const searchBar = page.getByTestId("main-search-bar");
10+
await expect(searchBar).toBeVisible();
11+
12+
await page.locator("body").click();
13+
14+
await page.keyboard.press("n");
15+
16+
await expect(searchBar).toBeFocused();
17+
await expect(searchBar).toHaveValue("n");
18+
});
19+
20+
test("a full search can be done via ambient key presses", async ({ page }) => {
21+
const searchBar = page.getByTestId("main-search-bar");
22+
await expect(searchBar).toBeVisible();
23+
24+
await page.locator("body").click();
25+
26+
await page.keyboard.type("p");
27+
28+
await expect(searchBar).toBeFocused();
29+
30+
// page.keyboard.type is too fast for Elm update so add a delay
31+
// delay 30, 100, 200 was not enough
32+
// await page.keyboard.type("python-web", { delay: 200 });
33+
await searchBar.fill("python-web");
34+
35+
await expect(searchBar).toHaveValue("python-web");
36+
37+
const results = page.getByTestId("app-result");
38+
await expect(results).toHaveCount(1);
39+
});
40+
41+
test("esc key should clear the search field", async ({ page }) => {
42+
const searchBar = page.getByTestId("main-search-bar");
43+
await expect(searchBar).toBeVisible();
44+
45+
await page.locator("body").click();
46+
47+
await page.keyboard.type("f");
48+
49+
await expect(searchBar).toBeFocused();
50+
51+
await searchBar.fill("foo:bar:baz");
52+
53+
await expect(searchBar).toHaveValue("foo:bar:baz");
54+
55+
let results = page.getByTestId("app-result");
56+
await expect(results).toHaveCount(0);
57+
58+
await page.keyboard.press("Escape");
59+
await expect(searchBar).toHaveValue("");
60+
61+
results = page.getByTestId("app-result");
62+
await expect(await results.count()).toBeGreaterThan(0);
63+
});
64+
});
65+
66+
test.describe("Ambient Search in App page", () => {
67+
test.beforeEach(async ({ page }) => {
68+
await page.goto("./");
69+
70+
const firstApp = page.getByTestId("app-result");
71+
await expect(firstApp).toBeVisible();
72+
await firstApp.click();
73+
});
74+
75+
test("ambient search works in app page", async ({ page }) => {
76+
const searchBar = page.getByTestId("main-search-bar");
77+
await expect(searchBar).toBeVisible();
78+
79+
await page.locator("body").click();
80+
81+
await page.keyboard.press("n");
82+
83+
await expect(searchBar).toBeFocused();
84+
await expect(searchBar).toHaveValue("n");
85+
});
86+
87+
test("ambient search is disabled when a modal is open", async ({ page }) => {
88+
const runBtn = page.getByTestId("app-run-button");
89+
await expect(runBtn).toBeVisible();
90+
await runBtn.click();
91+
92+
const modal = page.getByTestId("run-modal-container");
93+
await expect(modal).toBeVisible();
94+
95+
await page.keyboard.press("n");
96+
97+
const searchBar = page.getByTestId("main-search-bar");
98+
await expect(searchBar).not.toBeFocused();
99+
});
100+
101+
test("esc key will bring it back to app page for single keypress", async ({ page }) => {
102+
const currentAppPage = page.url();
103+
const searchBar = page.getByTestId("main-search-bar");
104+
await expect(searchBar).toBeVisible();
105+
106+
await page.locator("body").click();
107+
108+
await page.keyboard.press("n");
109+
110+
await expect(searchBar).toBeFocused();
111+
await expect(searchBar).toHaveValue("n");
112+
113+
await page.keyboard.press("Escape");
114+
await expect(searchBar).toHaveValue("");
115+
116+
await expect(page).toHaveURL(currentAppPage);
117+
});
118+
119+
test("esc key will bring it to apps list view after typing a word", async ({ page }) => {
120+
const searchBar = page.getByTestId("main-search-bar");
121+
await expect(searchBar).toBeVisible();
122+
123+
await page.locator("body").click();
124+
125+
await page.keyboard.press("s");
126+
127+
await expect(searchBar).toBeFocused();
128+
129+
await searchBar.fill("something");
130+
131+
await expect(searchBar).toHaveValue("something");
132+
133+
await page.keyboard.press("Escape");
134+
await expect(searchBar).toHaveValue("");
135+
136+
const results = page.getByTestId("app-result");
137+
await expect(await results.count()).toBeGreaterThan(0);
138+
});
139+
});
140+
141+
test.describe("Ambient Search in Recipe Options page", () => {
142+
test.beforeEach(async ({ page }) => {
143+
await page.goto("./recipe/options");
144+
});
145+
146+
test("typing anywhere auto-focuses the search bar and captures the key", async ({ page }) => {
147+
const searchBar = page.getByTestId("main-search-bar");
148+
await expect(searchBar).toBeVisible();
149+
150+
await page.locator("body").click();
151+
152+
await page.keyboard.press("a");
153+
154+
await expect(searchBar).toBeFocused();
155+
await expect(searchBar).toHaveValue("a");
156+
157+
await searchBar.fill("apps.*.description");
158+
159+
await expect(searchBar).toHaveValue("apps.*.description");
160+
161+
const results = page.getByTestId("option-result");
162+
await expect(results).toHaveCount(1);
163+
});
164+
});

0 commit comments

Comments
 (0)