Skip to content

Commit aea2c57

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

File tree

7 files changed

+224
-0
lines changed

7 files changed

+224
-0
lines changed

flake/develop/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
nodejs
2626
self'.packages.elm-watch
2727
self'.packages.elm2nix
28+
playwright-test
2829
systemd-manager-tui
2930
watchman
3031
podman-compose

ui/src/Main/View.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ viewSearchInput model =
143143
"Search apps"
144144
, value model.model_search
145145
, id "main-search-bar"
146+
, attribute "data-testid" "main-search-bar"
146147
, onInput (\s -> Update_SearchInput (UpdateSearchInput_Set s))
147148
, preventDefaultOn "keydown"
148149
(decodeEscapeKey
@@ -276,6 +277,7 @@ viewPageSearchApp model app =
276277
[ href (Route_App (initRouteApp app.app_name) |> Route.toString)
277278
, class "card m-app-card shadow-sm p-3"
278279
, style "text-decoration" "none"
280+
, attribute "data-testid" "app-result"
279281
, onClick (Update_Route (Route_App (initRouteApp app.app_name)))
280282
]
281283
[ div
@@ -357,6 +359,7 @@ viewPageAppHeader model pageApp =
357359
pageApp.pageApp_route
358360
in
359361
onClick (Update_RouteWithoutHistory (Route_App { route | routeApp_runShown = True }))
362+
, attribute "data-testid" "app-run-button"
360363
]
361364
[ text "Run" ]
362365
]
@@ -530,6 +533,7 @@ viewPageAppRun model pageApp =
530533
[ div
531534
[ class "modal show"
532535
, style "display" "block"
536+
, attribute "data-testid" "run-modal-container"
533537
, tabindex -1
534538
, style "background-color" "rgba(0,0,0,0.5)"
535539
, onClick (Update_RouteWithoutHistory onClickRoute)
@@ -677,6 +681,7 @@ viewPageRecipeOption model pageRecipeOptions ( optionName, option ) =
677681
[ class "recipe-option list-group-item list-group-item-action flex-column align-items-start"
678682
, href (onClickRoute |> Route.toString)
679683
, id optionName
684+
, attribute "data-testid" "option-result"
680685
, onClick (Update_Route onClickRoute)
681686
]
682687
[ div [ class "d-flex w-100 justify-content-between" ]

ui/tests/e2e/.gitignore

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

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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
// page.keyboard.type is too fast for Elm update so add a delay
27+
// delay 30, 100 was not enough
28+
await page.keyboard.type("python-web", { delay: 200 });
29+
30+
await expect(searchBar).toBeFocused();
31+
await expect(searchBar).toHaveValue("python-web");
32+
33+
const results = page.getByTestId("app-result");
34+
await expect(results).toHaveCount(1);
35+
});
36+
37+
test("esc key should clear the search field", async ({ page }) => {
38+
const searchBar = page.getByTestId("main-search-bar");
39+
await expect(searchBar).toBeVisible();
40+
41+
await page.locator("body").click();
42+
43+
await page.keyboard.type("non-existent-app-for-test", { delay: 200 });
44+
45+
await expect(searchBar).toBeFocused();
46+
await expect(searchBar).toHaveValue("non-existent-app-for-test");
47+
48+
let results = page.getByTestId("app-result");
49+
await expect(results).toHaveCount(0);
50+
51+
await page.keyboard.press("Escape");
52+
await expect(searchBar).toHaveValue("");
53+
54+
results = page.getByTestId("app-result");
55+
await expect(await results.count()).toBeGreaterThan(0);
56+
});
57+
});
58+
59+
test.describe("Ambient Search in App page", () => {
60+
test.beforeEach(async ({ page }) => {
61+
await page.goto("./");
62+
63+
const firstApp = page.getByTestId("app-result");
64+
await expect(firstApp).toBeVisible();
65+
await firstApp.click();
66+
});
67+
68+
test("ambient search works in app page", async ({ page }) => {
69+
const searchBar = page.getByTestId("main-search-bar");
70+
await expect(searchBar).toBeVisible();
71+
72+
await page.locator("body").click();
73+
74+
await page.keyboard.press("n");
75+
76+
await expect(searchBar).toBeFocused();
77+
await expect(searchBar).toHaveValue("n");
78+
});
79+
80+
test("ambient search is disabled when a modal is open", async ({ page }) => {
81+
const runBtn = page.getByTestId("app-run-button");
82+
await expect(runBtn).toBeVisible();
83+
await runBtn.click();
84+
85+
const modal = page.getByTestId("run-modal-container");
86+
await expect(modal).toBeVisible();
87+
88+
await page.keyboard.press("n");
89+
90+
const searchBar = page.getByTestId("main-search-bar");
91+
await expect(searchBar).not.toBeFocused();
92+
});
93+
94+
test("esc key will bring it back to app page for single keypress", async ({ page }) => {
95+
const currentAppPage = page.url();
96+
const searchBar = page.getByTestId("main-search-bar");
97+
await expect(searchBar).toBeVisible();
98+
99+
await page.locator("body").click();
100+
101+
await page.keyboard.press("n");
102+
103+
await expect(searchBar).toBeFocused();
104+
await expect(searchBar).toHaveValue("n");
105+
106+
await page.keyboard.press("Escape");
107+
await expect(searchBar).toHaveValue("");
108+
109+
await expect(page).toHaveURL(currentAppPage);
110+
});
111+
112+
test("esc key will bring it to apps list view after typing a word", async ({ page }) => {
113+
const searchBar = page.getByTestId("main-search-bar");
114+
await expect(searchBar).toBeVisible();
115+
116+
await page.locator("body").click();
117+
118+
await page.keyboard.type("something", { delay: 200 });
119+
120+
await expect(searchBar).toBeFocused();
121+
await expect(searchBar).toHaveValue("something");
122+
123+
await page.keyboard.press("Escape");
124+
await expect(searchBar).toHaveValue("");
125+
126+
const results = page.getByTestId("app-result");
127+
await expect(await results.count()).toBeGreaterThan(0);
128+
});
129+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("search works in homepage", async ({ page }) => {
4+
await page.goto("./");
5+
6+
const searchBar = page.getByTestId("main-search-bar");
7+
await searchBar.fill("python-web");
8+
9+
const results = page.getByTestId("app-result");
10+
11+
await expect(results).toHaveCount(1);
12+
});
13+
14+
test("search works in options page", async ({ page }) => {
15+
const responsePromise = page.waitForResponse(response => response.url().includes("forge-options.json"));
16+
17+
await page.goto("./recipe/options");
18+
19+
await responsePromise;
20+
21+
await expect(page.getByTestId("option-result").first()).toBeVisible();
22+
23+
const searchBar = page.getByTestId("main-search-bar");
24+
await searchBar.fill("apps.*.description");
25+
26+
const results = page.getByTestId("option-result");
27+
await expect(results).toHaveCount(1);
28+
});

ui/tests/e2e/specs/start.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("deployment reachable and Elm app mounts", async ({ page }) => {
4+
await page.goto("./");
5+
6+
await expect(page).toHaveTitle(/NGI Forge/i);
7+
});

0 commit comments

Comments
 (0)