-
Notifications
You must be signed in to change notification settings - Fork 1
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
mrc-4333 e2e tests #179
mrc-4333 e2e tests #179
Changes from all commits
d9030c4
401f806
f4c8cee
44810f1
999dfc1
858c3b7
1b70507
47b2c73
9110d07
24d37e7
f96ff73
65e5a64
5fb99dc
d1fc962
a433d1d
78b79cd
51c7f14
131f07e
45ed78f
b659293
c33c16e
46c86a1
72c2ae9
65470ce
f88ea33
42fa7b9
a8452ed
09c2362
7f14adb
fe74641
011e99b
929c9d4
688b118
47a41f4
a0a15ca
73a855a
2d6b370
373fa3c
831f778
1652e88
19472a7
c59b700
caf28a0
66a606f
746c90c
cf9be0b
d168b21
005330f
df25e94
15dee7e
ef187e3
a2d8065
12dbc8f
63c64f1
da88cfd
7cb2e42
ed09eb4
0eb3b65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
.idea | ||
.vscode | ||
packit.iml | ||
.token | ||
packit.iml | ||
app/test-results | ||
app/playwright-report | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
FROM node:18 | ||
FROM node:22 | ||
|
||
COPY package.json /app/package.json | ||
COPY package-lock.json /app/package-lock.json | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { test as setup, expect } from "./tagCheckFixture"; | ||
import { Page } from "@playwright/test"; | ||
|
||
const authMethodIsBasic = async (apiURL: string) => { | ||
const response = await fetch(`${apiURL}/auth/config`); | ||
if (!response.ok) { | ||
throw Error(`Unable to get auth type from api. Status was ${response.status}`); | ||
} | ||
const json = await response.json(); | ||
return json.enableBasicLogin; | ||
}; | ||
|
||
const getBasicCredentials = () => { | ||
const user = process.env.PACKIT_E2E_BASIC_USER; | ||
const password = process.env.PACKIT_E2E_BASIC_PASSWORD; | ||
|
||
if (!user || !password) { | ||
throw Error( | ||
"This packitserver uses Basic auth. Please set environment variables " + | ||
"PACKIT_E2E_BASIC_USER and PACKIT_E2E_BASIC_PASSWORD" | ||
); | ||
} | ||
|
||
return [user, password]; | ||
}; | ||
|
||
const doBasicLogin = async (user: string, password: string, page: Page) => { | ||
await page.goto("./"); | ||
await page.getByLabel("Email").fill(user); | ||
await page.getByLabel("Password").fill(password); | ||
await page.getByRole("button", { name: /Log in/i }).click(); | ||
}; | ||
|
||
const doGithubLogin = async (page: Page, apiURL: string) => { | ||
// github PAT may be set in env, otherwise login to github interactively | ||
let githubToken = process.env.GITHUB_ACCESS_TOKEN; | ||
|
||
if (!githubToken) { | ||
const aod = await import("@octokit/auth-oauth-device"); // This package requires dynamic import with our ts config | ||
const auth = aod.createOAuthDeviceAuth({ | ||
clientType: "oauth-app", | ||
clientId: "Ov23liUrbkR0qUtAO1zu", // Packit Oauth App | ||
scopes: ["read:org"], | ||
onVerification(verification) { | ||
console.log("Open %s", verification.verification_uri); | ||
console.log("Enter code: %s", verification.user_code); | ||
} | ||
}); | ||
|
||
const tokenAuthentication = await auth({ | ||
type: "oauth" | ||
}); | ||
githubToken = tokenAuthentication.token; | ||
} | ||
|
||
const packitResponse = await fetch(`${apiURL}/auth/login/api`, { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json" | ||
}, | ||
body: JSON.stringify({ token: githubToken }) | ||
}); | ||
if (!packitResponse.ok) { | ||
throw Error(`Unable to get token from api. Status was ${packitResponse.status}`); | ||
} | ||
const json = await packitResponse.json(); | ||
const packitToken = json.token; | ||
|
||
// Use redirect endpoint to login | ||
await page.goto(`./redirect?token=${packitToken}`); | ||
}; | ||
|
||
// Define "setup" as a dependency for any test project which requires prior authentication | ||
setup("authenticate", async ({ page, baseURL }, testInfo) => { | ||
console.log(`Authenticating with ${baseURL}`); | ||
// If baseURL is default localhost (used by CI), we assume that we're running locally with api on | ||
// port 8080, otherwise that api is accessible from baseURL/api | ||
const apiURL = baseURL === "http://localhost:3000/" ? "http://localhost:8080" : `${baseURL}/packit/api`; | ||
const basicAuth = await authMethodIsBasic(apiURL); | ||
if (basicAuth) { | ||
const [basicUser, basicPassword] = getBasicCredentials(); | ||
await doBasicLogin(basicUser, basicPassword, page); // get credentials interactively | ||
} else { | ||
await doGithubLogin(page, apiURL); | ||
} | ||
|
||
// Check login has succeeded - admin user should have user access role | ||
await expect(page.locator("body")).toHaveText(/Manage Access/); | ||
|
||
// write out context to tmp location to be picked up by dependent tests | ||
const authFile = testInfo.outputPath("auth.json"); | ||
await page.context().storageState({ path: authFile }); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { FullConfig } from "@playwright/test"; | ||
import { glob, readFile } from "fs/promises"; | ||
import { join } from "path"; | ||
|
||
// Ensure that all test files use the tagCheckFixture rather than playwright's default test fixture | ||
const checkTestImports = async (config: FullConfig) => { | ||
console.log("Checking test imports..."); | ||
const { rootDir } = config; | ||
const importRegex = /import\s+{[^}]*\btest\b[^}]*}\s+from\s+".\/tagCheckFixture";/; | ||
|
||
// Use a generic glob here which should pick up every test file - this is | ||
// simpler than checking testMatch for every project, which could be Regex or glob, or an | ||
// array of either. | ||
for await (const file of glob("**/*.@(spec|test).?(c|m)[jt]s?(x)", { cwd: rootDir })) { | ||
const filePath = join(rootDir, file); | ||
const content = await readFile(filePath, "utf-8"); | ||
if (!importRegex.test(content)) { | ||
throw new Error(`Test file ${file} does not import "test" from "tagCheckFixture".`); | ||
} | ||
} | ||
}; | ||
|
||
export default checkTestImports; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { test, expect } from "./tagCheckFixture"; | ||
import { Locator } from "@playwright/test"; | ||
import { | ||
getBreadcrumbLocator, | ||
getContentLocator, | ||
getReadableIdString, | ||
navigateToFirstPacketGroup, | ||
navigateToFirstPacketGroupLatestPacket, | ||
packetGroupNameFromListItem | ||
} from "./utils"; | ||
|
||
test.describe("Index page", () => { | ||
let content: Locator; | ||
let packetGroups: Locator; | ||
|
||
test.beforeEach(async ({ page }) => { | ||
await page.goto("./"); | ||
content = await getContentLocator(page); | ||
packetGroups = await content.getByRole("listitem"); | ||
expect(await packetGroups.count()).toBeGreaterThan(0); | ||
// wait for list items to not be skeletal | ||
await expect(await packetGroups.first().getByRole("heading")).toBeVisible(); | ||
}); | ||
|
||
test("can view packet group list", async () => { | ||
(await packetGroups.all()).forEach((packetGroup) => { | ||
expect(packetGroup.getByRole("heading")).toBeEnabled(); // packet group name | ||
expect(packetGroup.getByRole("link", { name: "Latest" })).toBeEnabled(); | ||
expect(packetGroup.getByText(/^\d+ packets?$/)).toBeVisible(); // packet count | ||
expect(packetGroup.getByText(/^Updated \d+ (second|minute|hour|day)s? ago$/)).toBeVisible(); // updated label | ||
}); | ||
}); | ||
|
||
test("can filter packet groups", async ({ page }) => { | ||
const firstPacketGroup = await packetGroups.first(); | ||
const firstPacketGroupName = await packetGroupNameFromListItem(firstPacketGroup); | ||
const filterInput = await page.getByPlaceholder("Filter packet groups..."); | ||
await filterInput.fill(firstPacketGroupName); | ||
// wait for reset-filter button to become visible | ||
await expect(await content.getByLabel("reset filter")).toBeVisible(); | ||
const filteredGroups = await content.getByRole("listitem"); | ||
// expect to have at least one packet group remaining, and expect all to have filter term as a substring | ||
expect(await filteredGroups.count()).toBeGreaterThan(0); | ||
for (const packetGroup of await filteredGroups.all()) { | ||
expect(await packetGroupNameFromListItem(packetGroup)).toContain(firstPacketGroupName); | ||
} | ||
Comment on lines
+45
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line failed for me when running the local e2e tests, so I re-ran the e2e tests 3 times but the failure never returned. The failure was:
Stacktrace showed that it was this second call to packetGroupNameFromListItem on line 41 that went wrong, not the first on line 32. The screenshot showed nothing out of the ordinary, so it probably just hit a timeout for some reason? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah maybe, just one of those great unexplained e2e oddities.. |
||
}); | ||
|
||
test("can navigate from packet group name link to packet group page", async ({ page }) => { | ||
const firstPacketGroupName = await navigateToFirstPacketGroup(content); | ||
const displayName = getReadableIdString(firstPacketGroupName); | ||
// wait for packet group name to be visible in breadcrumb | ||
await expect(await getBreadcrumbLocator(page)).toHaveText(`home${displayName}`); | ||
}); | ||
|
||
test("can navigate from latest packet link to packet page", async ({ page }) => { | ||
const { packetGroupName, packetId } = await navigateToFirstPacketGroupLatestPacket(content); | ||
// wait for packet group name and latest packet id to be visible in breadcrumb | ||
const displayPacketId = getReadableIdString(packetId); | ||
const displayPacketGroupName = getReadableIdString(packetGroupName); | ||
await expect(await getBreadcrumbLocator(page)).toHaveText(`home${displayPacketGroupName}${displayPacketId}`); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would these be useful to upload as artefacts of the CI?
eg.
packit/.github/workflows/backend-test-and-build.yml
Lines 72 to 81 in 377f8e4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that
auth.json
is written to thetest-results
dir, so if that is erroneously not deleted (e.g. if playwright dies mid-run), we'd be uploading the github access token for all to see. Soauth.json
might want to be written somewhere else instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't be the github access token, it would just be the temporary packit token, which would only apply to the localhost instance we're running on CI, so I think we should be ok unless we start running tests against prod on CI. I'll try to omit the auth.json though. (Playwright doesn't let you write outside the parent folder, but I'll try deleting it before upload.