kunai-runner 6.10.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +68 -0
- package/README.md +17 -0
- package/docs/01_shibboleth_auth.md +122 -0
- package/docs/02_versioning_and_release.md +146 -0
- package/docs/backlog.md +71 -0
- package/docs/combine_videos.md +109 -0
- package/docs/test_data.md +34 -0
- package/lib/auth-file.ts +22 -0
- package/lib/login-adapters/builtin.ts +42 -0
- package/lib/login-adapters/duo-mfa.ts +41 -0
- package/lib/login-adapters/incommon-seamlessaccess.ts +56 -0
- package/lib/login-adapters/index.ts +68 -0
- package/lib/login-adapters/shibboleth-direct.ts +34 -0
- package/lib/login-adapters/types.ts +21 -0
- package/package.json +51 -0
- package/playwright.config.ts +206 -0
- package/scripts/combine_videos.py +250 -0
- package/tests/suite/01-preflight.spec.ts +147 -0
- package/tests/suite/02-account-management.spec.ts +113 -0
- package/tests/suite/03-create-dataverse.spec.ts +114 -0
- package/tests/suite/04-publish-dataverse.spec.ts +33 -0
- package/tests/suite/05-theme-widgets.spec.ts +65 -0
- package/tests/suite/06-theme-widgets-edit.spec.ts +60 -0
- package/tests/suite/10-assign-user-group-roles.spec.ts +34 -0
- package/tests/suite/11-create-edit-metadata-template.spec.ts +61 -0
- package/tests/suite/12-create-dataverse-collection.spec.ts +27 -0
- package/tests/suite/13-dataset-actions.spec.ts +105 -0
- package/tests/suite/14-browse-dataset-records.spec.ts +32 -0
- package/tests/suite/15-search-dataset-records.spec.ts +26 -0
- package/tests/suite/16-view-dataset-version-history.spec.ts +28 -0
- package/tests/suite/17-download-dataset-files.spec.ts +35 -0
- package/tests/suite/assets/footer.png +0 -0
- package/tests/suite/assets/logo.png +0 -0
- package/tests/suite/assets/thumbnail.png +0 -0
- package/tests/suite/auth.setup.ts +71 -0
- package/tests/suite/s02-state.ts +35 -0
- package/tests/suite/test-data/replaced-sample-dataset-file.txt +7 -0
- package/tests/suite/test-data/sample-dataset-file-2.txt +7 -0
- package/tests/suite/test-data/sample-dataset-file.txt +7 -0
- package/tests/suite/tsconfig.json +10 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
const process = (globalThis as any).process;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @tags @21cfr
|
|
6
|
+
*
|
|
7
|
+
* 21 CFR Part 11 — Tests #7–#11
|
|
8
|
+
* Creates a dataset, edits metadata, edits file metadata, replaces a file,
|
|
9
|
+
* and publishes the dataset.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"21 CFR: Dataset actions (create, edit, replace, publish)",
|
|
14
|
+
{ tag: ["@21cfr"] },
|
|
15
|
+
async ({ page }) => {
|
|
16
|
+
await page.goto(process.env.ROOT_DATAVERSE ?? "/");
|
|
17
|
+
|
|
18
|
+
// ─── Test #7 — Create Dataset ───
|
|
19
|
+
await page.getByRole("button", { name: "Add Data" }).click();
|
|
20
|
+
await page.getByRole("link", { name: "New Dataset" }).click();
|
|
21
|
+
await page
|
|
22
|
+
.locator('[id$=":0:inputText"]')
|
|
23
|
+
.first()
|
|
24
|
+
.type("Playwright Test Dataset");
|
|
25
|
+
await page
|
|
26
|
+
.locator('[id$=":0:description"]')
|
|
27
|
+
.first()
|
|
28
|
+
.type(
|
|
29
|
+
"This is a dummy dataset created by Playwright for testing purposes.",
|
|
30
|
+
);
|
|
31
|
+
await page
|
|
32
|
+
.locator(".ui-selectcheckboxmenu-multiple-container")
|
|
33
|
+
.first()
|
|
34
|
+
.click();
|
|
35
|
+
|
|
36
|
+
await page
|
|
37
|
+
.locator(".ui-selectcheckboxmenu-items-wrapper")
|
|
38
|
+
.first()
|
|
39
|
+
.getByText("Chemistry")
|
|
40
|
+
.click();
|
|
41
|
+
|
|
42
|
+
await page
|
|
43
|
+
.locator('[id="datasetForm:fileUpload_input"]')
|
|
44
|
+
.setInputFiles([
|
|
45
|
+
"tests/suite/test-data/sample-dataset-file.txt",
|
|
46
|
+
"tests/suite/test-data/sample-dataset-file-2.txt",
|
|
47
|
+
]);
|
|
48
|
+
await page.waitForTimeout(3000);
|
|
49
|
+
await page.getByRole("button", { name: "Save Dataset" }).click();
|
|
50
|
+
|
|
51
|
+
// ─── Test #8 — Edit Dataset Metadata ───
|
|
52
|
+
await page
|
|
53
|
+
.getByRole("button", { name: "Edit Dataset" })
|
|
54
|
+
.click({ force: true });
|
|
55
|
+
await page.locator("id=datasetForm:editMetadata").dispatchEvent("click");
|
|
56
|
+
await page
|
|
57
|
+
.locator('[id$=":0:inputText"]')
|
|
58
|
+
.first()
|
|
59
|
+
.fill("Playwright Test Dataset Modified");
|
|
60
|
+
await page.getByRole("button", { name: "Save Changes" }).last().click();
|
|
61
|
+
|
|
62
|
+
// ─── Test #9 — Edit File Metadata ───
|
|
63
|
+
await page.locator(".ui-chkbox-all").first().click();
|
|
64
|
+
await page.getByRole("button", { name: "Edit Files" }).click();
|
|
65
|
+
await page
|
|
66
|
+
.getByRole("link", { name: "Metadata" })
|
|
67
|
+
.last()
|
|
68
|
+
.click({ force: true });
|
|
69
|
+
await page
|
|
70
|
+
.locator('[name="datasetForm:filesTable:0:fileDescription"]')
|
|
71
|
+
.type("This is a modified description for the dataset file.");
|
|
72
|
+
await page.getByRole("button", { name: "Save Changes" }).last().click();
|
|
73
|
+
|
|
74
|
+
// ─── Test #10 — Replace File ───
|
|
75
|
+
await page.getByRole("link", { name: "sample-dataset-file.txt" }).click();
|
|
76
|
+
await page.getByRole("button", { name: "Edit File" }).click();
|
|
77
|
+
await page.getByRole("link", { name: "Replace" }).click();
|
|
78
|
+
await page
|
|
79
|
+
.locator('[id="datasetForm:fileUpload_input"]')
|
|
80
|
+
.setInputFiles([
|
|
81
|
+
"tests/suite/test-data/replaced-sample-dataset-file.txt",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const saveButton = page
|
|
85
|
+
.getByRole("button", { name: "Save Changes" })
|
|
86
|
+
.last();
|
|
87
|
+
if (await saveButton.isVisible()) {
|
|
88
|
+
await saveButton.click();
|
|
89
|
+
} else {
|
|
90
|
+
console.error(
|
|
91
|
+
"ERROR: Save Changes button not visible after file replacement. " +
|
|
92
|
+
"21 CFR Part 11 compliance may not be fully met. Proceeding with bypass.",
|
|
93
|
+
);
|
|
94
|
+
await page.getByRole("button", { name: "Done" }).click();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Test #11 — Publish Dataset ───
|
|
98
|
+
await page
|
|
99
|
+
.getByRole("link", { name: "Playwright Test Dataset Modified" })
|
|
100
|
+
.click();
|
|
101
|
+
await page.getByRole("link", { name: "Publish Dataset" }).click();
|
|
102
|
+
await page.getByRole("button", { name: "Continue" }).click();
|
|
103
|
+
console.log("Dataset finalized and published.");
|
|
104
|
+
},
|
|
105
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
const process = (globalThis as any).process;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @tags @21cfr
|
|
6
|
+
*
|
|
7
|
+
* 21 CFR Part 11 — Test #12
|
|
8
|
+
* Sorts the results table by name, opens the first dataset record, and
|
|
9
|
+
* navigates to its Metadata tab.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"21 CFR: Browse dataset records",
|
|
14
|
+
{ tag: ["@21cfr"] },
|
|
15
|
+
async ({ page }) => {
|
|
16
|
+
await page.goto(process.env.ROOT_DATAVERSE ?? "/");
|
|
17
|
+
await page.getByRole("button", { name: "Sort" }).click();
|
|
18
|
+
await page.getByRole("link", { name: "Name" }).first().click();
|
|
19
|
+
// Click the "Toggle Collections" checkbox anchor to filter to datasets only.
|
|
20
|
+
await page.locator('a.facetTypeChBox[href*="types=datasets"]').click();
|
|
21
|
+
await page.waitForLoadState("domcontentloaded");
|
|
22
|
+
await page
|
|
23
|
+
.locator("#resultsTable")
|
|
24
|
+
.locator("tbody")
|
|
25
|
+
.locator("tr")
|
|
26
|
+
.first()
|
|
27
|
+
.locator("a")
|
|
28
|
+
.first()
|
|
29
|
+
.click();
|
|
30
|
+
await page.getByRole("link", { name: "Metadata" }).click();
|
|
31
|
+
},
|
|
32
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
const process = (globalThis as any).process;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @tags @21cfr
|
|
6
|
+
*
|
|
7
|
+
* 21 CFR Part 11 — Test #13
|
|
8
|
+
* Searches for "Playwright" using the dataverse search box, then performs
|
|
9
|
+
* an advanced search and asserts the URL reflects the query.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"21 CFR: Search dataset records",
|
|
14
|
+
{ tag: ["@21cfr"] },
|
|
15
|
+
async ({ page }) => {
|
|
16
|
+
await page.goto(process.env.ROOT_DATAVERSE ?? "/");
|
|
17
|
+
await page.getByPlaceholder("Search this dataverse").fill("Playwright");
|
|
18
|
+
await page.getByRole("link", { name: "Find" }).click();
|
|
19
|
+
await page.getByRole("link", { name: "Advanced Search" }).click();
|
|
20
|
+
await page
|
|
21
|
+
.locator('[id="advancedSearchForm:dvFieldName"]')
|
|
22
|
+
.fill("Playwright");
|
|
23
|
+
await page.getByRole("button", { name: "Find" }).last().click();
|
|
24
|
+
await expect(page).toHaveURL(/Playwright/);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
const process = (globalThis as any).process;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @tags @21cfr
|
|
6
|
+
*
|
|
7
|
+
* 21 CFR Part 11 — Test #14
|
|
8
|
+
* Opens the first dataset in the results table, navigates to its Versions
|
|
9
|
+
* tab, and clicks through to version 1.0.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"21 CFR: View dataset version history",
|
|
14
|
+
{ tag: ["@21cfr"] },
|
|
15
|
+
async ({ page }) => {
|
|
16
|
+
await page.goto(process.env.ROOT_DATAVERSE ?? "/");
|
|
17
|
+
await page
|
|
18
|
+
.locator("#resultsTable")
|
|
19
|
+
.locator("tbody")
|
|
20
|
+
.locator("tr")
|
|
21
|
+
.first()
|
|
22
|
+
.locator("a")
|
|
23
|
+
.first()
|
|
24
|
+
.click();
|
|
25
|
+
await page.getByRole("link", { name: "Versions" }).click();
|
|
26
|
+
await page.getByRole("link", { name: "1.0" }).last().click();
|
|
27
|
+
},
|
|
28
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
const process = (globalThis as any).process;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @tags @21cfr
|
|
6
|
+
*
|
|
7
|
+
* 21 CFR Part 11 — Test #15
|
|
8
|
+
* Opens the first dataset in the results table, selects all files, triggers
|
|
9
|
+
* a download, and verifies the downloaded filename is non-null.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"21 CFR: Download dataset files",
|
|
14
|
+
{ tag: ["@21cfr"] },
|
|
15
|
+
async ({ page }) => {
|
|
16
|
+
await page.goto(process.env.ROOT_DATAVERSE ?? "/");
|
|
17
|
+
|
|
18
|
+
await page
|
|
19
|
+
.locator("#resultsTable tbody tr")
|
|
20
|
+
.first()
|
|
21
|
+
.locator("a")
|
|
22
|
+
.first()
|
|
23
|
+
.click();
|
|
24
|
+
|
|
25
|
+
await page.locator(".ui-chkbox-all").first().click();
|
|
26
|
+
|
|
27
|
+
const downloadPromise = page.waitForEvent("download");
|
|
28
|
+
await page.getByRole("link", { name: "Download" }).click();
|
|
29
|
+
const download = await downloadPromise;
|
|
30
|
+
|
|
31
|
+
const fileName = download.suggestedFilename();
|
|
32
|
+
console.log(`Downloaded file: ${fileName}`);
|
|
33
|
+
expect(fileName).not.toBeNull();
|
|
34
|
+
},
|
|
35
|
+
);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { test as setup } from "@playwright/test";
|
|
2
|
+
import { getLoginAdapter } from "../../lib/login-adapters";
|
|
3
|
+
import { authFilePath } from "../../lib/auth-file";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
const process = (globalThis as any).process;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads a required environment variable, throwing a clear error if absent.
|
|
10
|
+
*/
|
|
11
|
+
function requireEnv(name: string): string {
|
|
12
|
+
const value = process.env[name];
|
|
13
|
+
if (!value) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Missing required environment variable "${name}". ` +
|
|
16
|
+
`Add it to your .env file (see .env.example).`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setup("Authenticate", async ({ page, browserName }) => {
|
|
23
|
+
// Each browser gets its own auth file so sessions never collide.
|
|
24
|
+
const authFile = authFilePath(browserName);
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(authFile)) {
|
|
27
|
+
await page
|
|
28
|
+
.context()
|
|
29
|
+
.addCookies(JSON.parse(fs.readFileSync(authFile, "utf-8")).cookies);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await page.goto("/");
|
|
33
|
+
|
|
34
|
+
// Dismiss cookie consent banner if present
|
|
35
|
+
const cookieConsent = page.locator('button[data-role="all"]');
|
|
36
|
+
if (await cookieConsent.isVisible()) {
|
|
37
|
+
await cookieConsent.click();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for the user's full name inside the navbar user display element.
|
|
41
|
+
// Scoping to #userDisplayInfoTitle prevents false positives when the name
|
|
42
|
+
// appears in dataset titles or other page content.
|
|
43
|
+
const fullName = requireEnv("DV_FULL_NAME");
|
|
44
|
+
const fullNameVisible = await page
|
|
45
|
+
.locator("#userDisplayInfoTitle")
|
|
46
|
+
.getByText(fullName, { exact: false })
|
|
47
|
+
.isVisible()
|
|
48
|
+
.catch(() => false);
|
|
49
|
+
|
|
50
|
+
if (fullNameVisible) {
|
|
51
|
+
console.log("Already logged in. Skipping authentication.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("Not logged in. Starting authentication via adapter.");
|
|
56
|
+
|
|
57
|
+
// Use .first() — some instances render two "Log In" links (navbar + mega-menu).
|
|
58
|
+
const loginButton = page
|
|
59
|
+
.getByRole("link", { name: "Log In", exact: true })
|
|
60
|
+
.first();
|
|
61
|
+
await loginButton.click({ force: true });
|
|
62
|
+
|
|
63
|
+
const adapter = getLoginAdapter();
|
|
64
|
+
await adapter.login(page, {
|
|
65
|
+
username: requireEnv("DV_USERNAME"),
|
|
66
|
+
password: requireEnv("DV_PASSWORD"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync("playwright/.auth", { recursive: true });
|
|
70
|
+
await page.context().storageState({ path: authFile });
|
|
71
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny persistence helper for the Section 2 dataverse identifier.
|
|
3
|
+
*
|
|
4
|
+
* The identifier is written to a local temp file that is git-ignored.
|
|
5
|
+
* This lets the create-dataverse test write the identifier once and all
|
|
6
|
+
* subsequent Section 2 tests (publish, theme, etc.) consume it even when
|
|
7
|
+
* run independently.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
const STATE_FILE = path.resolve(process.cwd(), ".s2-dataverse-id");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persist the dataverse identifier to disk so later tests can consume it.
|
|
16
|
+
* Called once at the end of 05-create-dataverse.
|
|
17
|
+
*/
|
|
18
|
+
export function writeDataverseId(id: string): void {
|
|
19
|
+
fs.writeFileSync(STATE_FILE, id, "utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the dataverse identifier written by 05-create-dataverse.
|
|
24
|
+
* Throws a helpful error if the file doesn't exist (i.e. the create test
|
|
25
|
+
* has never run in this workspace).
|
|
26
|
+
*/
|
|
27
|
+
export function readDataverseId(): string {
|
|
28
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Dataverse identifier state file not found at:\n ${STATE_FILE}\n` +
|
|
31
|
+
`Run 05-create-dataverse.spec.ts first to generate it.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return fs.readFileSync(STATE_FILE, "utf-8").trim();
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
This is a REPLACED sample dataset file used by Playwright to test and generate datasets.
|
|
2
|
+
|
|
3
|
+
You can verify the MD5 of this file as needed using node.js crypto. It is recommended NOT to use MD5 due to collision vulnerabilities.
|
|
4
|
+
|
|
5
|
+
Going forward, you may prefer to use SHA3-384 or SHA3-512 or Argon2id.
|
|
6
|
+
|
|
7
|
+
Please refer to the latest NIST requirements about hashing.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
This is a sample dataset file number 2 used by Playwright to test and generate datasets.
|
|
2
|
+
|
|
3
|
+
You can verify the MD5 of this file as needed using node.js crypto. It is recommended NOT to use MD5 due to collision vulnerabilities.
|
|
4
|
+
|
|
5
|
+
Going forward, you may prefer to use SHA3-384 or SHA3-512 or Argon2id.
|
|
6
|
+
|
|
7
|
+
Please refer to the latest NIST requirements about hashing.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
This is a sample dataset file used by Playwright to test and generate datasets.
|
|
2
|
+
|
|
3
|
+
You can verify the MD5 of this file as needed using node.js crypto. It is recommended NOT to use MD5 due to collision vulnerabilities.
|
|
4
|
+
|
|
5
|
+
Going forward, you may prefer to use SHA3-384 or SHA3-512 or Argon2id.
|
|
6
|
+
|
|
7
|
+
Please refer to the latest NIST requirements about hashing.
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"ignoreDeprecations": "6.0",
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["playwright.config.ts", "tests/**/*.ts", "lib/**/*.ts"]
|
|
15
|
+
}
|