pomwright 1.3.0 → 1.5.0

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.
Files changed (117) hide show
  1. package/AGENTS.md +37 -0
  2. package/CHANGELOG.md +193 -0
  3. package/README.md +316 -34
  4. package/dist/index.d.mts +1058 -132
  5. package/dist/index.d.ts +1058 -132
  6. package/dist/index.js +2309 -185
  7. package/dist/index.mjs +2304 -185
  8. package/docs/{get-locator-methods-explanation.md → v1/get-locator-methods-explanation.md} +0 -16
  9. package/docs/v1-to-v2-migration/bridge-migration-guide.md +159 -0
  10. package/docs/v1-to-v2-migration/direct-migration-guide.md +238 -0
  11. package/docs/v1-to-v2-migration/v1-to-v2-comparison.md +547 -0
  12. package/docs/v2/PageObject.md +293 -0
  13. package/docs/v2/composing-locator-modules.md +93 -0
  14. package/docs/v2/locator-registry.md +693 -0
  15. package/docs/v2/logging.md +168 -0
  16. package/docs/v2/overview.md +515 -0
  17. package/docs/v2/session-storage.md +160 -0
  18. package/index.ts +61 -9
  19. package/intTestV2/.env +0 -0
  20. package/intTestV2/fixtures/testApp.fixtures.ts +43 -0
  21. package/intTestV2/package.json +22 -0
  22. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.locatorSchema.ts +24 -0
  23. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.page.ts +17 -0
  24. package/intTestV2/page-object-models/testApp/pages/testPage.locatorSchema.ts +32 -0
  25. package/intTestV2/page-object-models/testApp/pages/testPage.page.ts +119 -0
  26. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.locatorSchema.ts +29 -0
  27. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.page.ts +48 -0
  28. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.locatorSchema.ts +9 -0
  29. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.page.ts +23 -0
  30. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.locatorSchema.ts +114 -0
  31. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.page.ts +23 -0
  32. package/intTestV2/page-object-models/testApp/testApp.base.ts +20 -0
  33. package/intTestV2/playwright.config.ts +54 -0
  34. package/intTestV2/server.js +216 -0
  35. package/intTestV2/test-data/staticPage/index.html +280 -0
  36. package/intTestV2/test-data/staticPage/w3images/avatar2.png +0 -0
  37. package/intTestV2/test-data/staticPage/w3images/avatar3.png +0 -0
  38. package/intTestV2/test-data/staticPage/w3images/avatar5.png +0 -0
  39. package/intTestV2/test-data/staticPage/w3images/avatar6.png +0 -0
  40. package/intTestV2/test-data/staticPage/w3images/forest.jpg +0 -0
  41. package/intTestV2/test-data/staticPage/w3images/lights.jpg +0 -0
  42. package/intTestV2/test-data/staticPage/w3images/mountains.jpg +0 -0
  43. package/intTestV2/test-data/staticPage/w3images/nature.jpg +0 -0
  44. package/intTestV2/test-data/staticPage/w3images/snow.jpg +0 -0
  45. package/intTestV2/tests/locatorRegistry/add/add.describe.spec.ts +54 -0
  46. package/intTestV2/tests/locatorRegistry/add/add.filter.spec.ts +143 -0
  47. package/intTestV2/tests/locatorRegistry/add/add.frameLocator.spec.ts +23 -0
  48. package/intTestV2/tests/locatorRegistry/add/add.getByAltText.spec.ts +23 -0
  49. package/intTestV2/tests/locatorRegistry/add/add.getById.spec.ts +45 -0
  50. package/intTestV2/tests/locatorRegistry/add/add.getByLabel.spec.ts +23 -0
  51. package/intTestV2/tests/locatorRegistry/add/add.getByPlaceholder.spec.ts +23 -0
  52. package/intTestV2/tests/locatorRegistry/add/add.getByRole.spec.ts +23 -0
  53. package/intTestV2/tests/locatorRegistry/add/add.getByTestId.spec.ts +23 -0
  54. package/intTestV2/tests/locatorRegistry/add/add.getByText.spec.ts +23 -0
  55. package/intTestV2/tests/locatorRegistry/add/add.getByTitle.spec.ts +23 -0
  56. package/intTestV2/tests/locatorRegistry/add/add.locator.spec.ts +23 -0
  57. package/intTestV2/tests/locatorRegistry/add/add.reuseExisting.spec.ts +66 -0
  58. package/intTestV2/tests/locatorRegistry/add/add.reuseReusable.spec.ts +311 -0
  59. package/intTestV2/tests/locatorRegistry/add/add.spec.ts +159 -0
  60. package/intTestV2/tests/locatorRegistry/filter.cycle.spec.ts +39 -0
  61. package/intTestV2/tests/locatorRegistry/getLocator/getLocator.spec.ts +253 -0
  62. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.clearSteps.spec.ts +105 -0
  63. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.describe.spec.ts +23 -0
  64. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.filter.spec.ts +368 -0
  65. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getLocator.spec.ts +56 -0
  66. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getNestedLocator.spec.ts +175 -0
  67. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.nth.spec.ts +60 -0
  68. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.remove.spec.ts +32 -0
  69. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.replace.spec.ts +24 -0
  70. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.spec.ts +110 -0
  71. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.update.spec.ts +322 -0
  72. package/intTestV2/tests/locatorRegistry/getNestedLocator/getNestedLocator.spec.ts +412 -0
  73. package/intTestV2/tests/locatorRegistry/registry/registry.binding.spec.ts +50 -0
  74. package/intTestV2/tests/locatorRegistry/validation/validation.locatorSchemaPath.spec.ts +115 -0
  75. package/intTestV2/tests/locatorRegistry/validation/validation.sub-path.spec.ts +45 -0
  76. package/intTestV2/tests/step/step.spec.ts +49 -0
  77. package/intTestV2/tests/testApp/color.spec.ts +15 -0
  78. package/intTestV2/tests/testApp/iframe.spec.ts +57 -0
  79. package/intTestV2/tests/testApp/testFilters.spec.ts +24 -0
  80. package/intTestV2/tests/testApp/testPage.spec.ts +161 -0
  81. package/intTestV2/tests/testApp/testPath.spec.ts +18 -0
  82. package/pack-build.sh +11 -0
  83. package/pack-test-v2.sh +36 -0
  84. package/package.json +10 -3
  85. package/playwright.base.ts +42 -0
  86. package/skills/README.md +56 -0
  87. package/skills/pomwright-v1-5-bridge-migration/SKILL.md +40 -0
  88. package/skills/pomwright-v1-5-bridge-migration/references/call-site-migration.md +178 -0
  89. package/skills/pomwright-v1-5-bridge-migration/references/schema-translation.md +183 -0
  90. package/skills/pomwright-v2-migration/SKILL.md +63 -0
  91. package/skills/pomwright-v2-migration/references/call-site-migration.md +265 -0
  92. package/skills/pomwright-v2-migration/references/class-migration.md +266 -0
  93. package/skills/pomwright-v2-migration/references/fixture-and-helpers.md +423 -0
  94. package/skills/pomwright-v2-migration/references/locator-registration.md +344 -0
  95. package/srcV2/fixture/base.fixtures.ts +23 -0
  96. package/srcV2/helpers/navigation.ts +153 -0
  97. package/srcV2/helpers/playwrightReportLogger.ts +196 -0
  98. package/srcV2/helpers/sessionStorage.ts +251 -0
  99. package/srcV2/helpers/stepDecorator.ts +106 -0
  100. package/srcV2/locators/index.ts +15 -0
  101. package/srcV2/locators/locatorQueryBuilder.ts +427 -0
  102. package/srcV2/locators/locatorRegistrationBuilder.ts +558 -0
  103. package/srcV2/locators/locatorRegistry.ts +541 -0
  104. package/srcV2/locators/locatorUpdateBuilder.ts +602 -0
  105. package/srcV2/locators/reusableLocatorBuilder.ts +200 -0
  106. package/srcV2/locators/types.ts +256 -0
  107. package/srcV2/locators/utils.ts +309 -0
  108. package/srcV2/locators/v1SchemaTranslator.ts +178 -0
  109. package/srcV2/pageObject.ts +105 -0
  110. /package/docs/{BaseApi-explanation.md → v1/BaseApi-explanation.md} +0 -0
  111. /package/docs/{BasePage-explanation.md → v1/BasePage-explanation.md} +0 -0
  112. /package/docs/{LocatorSchema-explanation.md → v1/LocatorSchema-explanation.md} +0 -0
  113. /package/docs/{LocatorSchemaPath-explanation.md → v1/LocatorSchemaPath-explanation.md} +0 -0
  114. /package/docs/{PlaywrightReportLogger-explanation.md → v1/PlaywrightReportLogger-explanation.md} +0 -0
  115. /package/docs/{intro-to-using-pomwright.md → v1/intro-to-using-pomwright.md} +0 -0
  116. /package/docs/{sessionStorage-methods-explanation.md → v1/sessionStorage-methods-explanation.md} +0 -0
  117. /package/docs/{tips-folder-structure.md → v1/tips-folder-structure.md} +0 -0
@@ -0,0 +1,114 @@
1
+ import type { LocatorRegistry } from "pomwright";
2
+
3
+ export type Paths =
4
+ | "one"
5
+ | "one.two"
6
+ | "body"
7
+ | "body.section"
8
+ | "body.section.heading"
9
+ | "body.section.button"
10
+ | "body.section@playground"
11
+ | "body.section@playground.heading"
12
+ | "body.section@playground.button"
13
+ | "body.section@playground.button@red"
14
+ | "body.section@playground.button@reset"
15
+ /**
16
+ * Fictional LocatorSchema do not exist in the DOM of the /testfilters page:
17
+ */
18
+ | "fictional.filter@undefined"
19
+ | "fictional.filter@optionsUndefined"
20
+ | "fictional.locatorWithfilter@allOptions"
21
+ | "fictional.locatorAndOptionsWithfilter@allOptions"
22
+ | "fictional.filter@has"
23
+ | "fictional.filter@hasNot"
24
+ | "fictional.filter@hasText"
25
+ | "fictional.filter@hasNotText"
26
+ | "fictional.filter@hasNotText.filter@hasText"
27
+ | "fictional.filter@hasNotText.filter@hasText.filter@hasNotText"
28
+ | "fictional.filter@hasNotText.filter@hasText.filter@hasNotText.filter@hasText";
29
+
30
+ export function defineLocators(registry: LocatorRegistry<Paths>) {
31
+ registry.add("one").locator("div.one");
32
+
33
+ registry.add("one.two").locator("div.two").filter({ hasText: "two" }).nth(0);
34
+
35
+ registry.add("body").locator("body");
36
+
37
+ registry.add("body.section").locator("section");
38
+
39
+ registry.add("body.section.heading").getByRole("heading", { level: 2 });
40
+
41
+ registry.add("body.section.button").getByRole("button");
42
+
43
+ registry.add("body.section@playground").locator("section", { hasText: /Playground/i });
44
+
45
+ registry
46
+ .add("body.section@playground.heading")
47
+ .getByRole("heading", { name: "Primary Colors Playground", level: 2, exact: true });
48
+
49
+ registry.add("body.section@playground.button").getByRole("button");
50
+
51
+ registry.add("body.section@playground.button@red").getByRole("button", { name: "Red" });
52
+
53
+ registry.add("body.section@playground.button@reset").getByRole("button", { name: "Reset Color" });
54
+
55
+ /** --------------------------- Fictional LocatorSchema ---------------------------
56
+ * The following LocatorSchema DO NOT exist in the DOM of the /testfilters page
57
+ *
58
+ * They are used for testing that the getNestedLocator/getLocator methods correctly
59
+ * produces correct locator selector strings for LocatorSchema with/without filter.
60
+ */
61
+
62
+ registry.add("fictional.filter@undefined").getByRole("button");
63
+
64
+ registry
65
+ .add("fictional.filter@optionsUndefined")
66
+ .getByRole("button")
67
+ .filter({ has: undefined, hasNot: undefined, hasText: undefined, hasNotText: undefined });
68
+
69
+ registry
70
+ .add("fictional.locatorWithfilter@allOptions")
71
+ .getByRole("button")
72
+ .filter({
73
+ has: "body.section",
74
+ hasNot: "body.section.button",
75
+ hasText: "hasText",
76
+ hasNotText: "hasNotText",
77
+ });
78
+
79
+ registry
80
+ .add("fictional.locatorAndOptionsWithfilter@allOptions")
81
+ .getByRole("button", { name: "roleOptions" })
82
+ .filter({
83
+ has: "body.section.heading",
84
+ hasNot: "body.section.button",
85
+ hasText: "hasText",
86
+ hasNotText: "hasNotText",
87
+ });
88
+
89
+ registry
90
+ .add("fictional.filter@has")
91
+ .getByRole("button")
92
+ .filter({ has: "body.section.heading" });
93
+
94
+ registry
95
+ .add("fictional.filter@hasNot")
96
+ .getByRole("button")
97
+ .filter({ hasNot: "body.section.button" });
98
+
99
+ registry.add("fictional.filter@hasText").getByRole("button").filter({ hasText: "hasText" });
100
+
101
+ registry.add("fictional.filter@hasNotText").getByRole("button").filter({ hasNotText: "hasNotText" });
102
+
103
+ registry.add("fictional.filter@hasNotText.filter@hasText").getByRole("button").filter({ hasText: "hasText" });
104
+
105
+ registry
106
+ .add("fictional.filter@hasNotText.filter@hasText.filter@hasNotText")
107
+ .getByRole("button")
108
+ .filter({ hasNotText: "hasNotText" });
109
+
110
+ registry
111
+ .add("fictional.filter@hasNotText.filter@hasText.filter@hasNotText.filter@hasText")
112
+ .getByRole("button")
113
+ .filter({ hasText: "hasText" });
114
+ }
@@ -0,0 +1,23 @@
1
+ import { type Page, test } from "@playwright/test";
2
+ import TestApp from "../../testApp.base";
3
+ import { defineLocators, type Paths } from "./testfilters.locatorSchema";
4
+
5
+ export default class TestFiltersV2 extends TestApp<Paths> {
6
+ constructor(page: Page) {
7
+ super(page, "/testfilters");
8
+ }
9
+
10
+ protected defineLocators(): void {
11
+ defineLocators(this.locatorRegistry);
12
+ }
13
+
14
+ protected pageActionsToPerformAfterNavigation(): (() => Promise<void>)[] | null {
15
+ return [];
16
+ }
17
+
18
+ async expectThisPage() {
19
+ await test.step(`Expect Page: ${this.urlPath}`, async () => {
20
+ await this.page.waitForURL(this.fullUrl);
21
+ });
22
+ }
23
+ }
@@ -0,0 +1,20 @@
1
+ import type { Page } from "@playwright/test";
2
+ import { type NavigationOptions, PageObject, type UrlPathTypeFromOptions, type UrlTypeOptions } from "pomwright";
3
+
4
+ type BaseOptions<Options extends UrlTypeOptions> = {
5
+ baseUrlType: string;
6
+ urlPathType: UrlPathTypeFromOptions<Options>;
7
+ };
8
+
9
+ export default abstract class TestApp<
10
+ Paths extends string,
11
+ Options extends UrlTypeOptions = { baseUrlType: string; urlPathType: string },
12
+ > extends PageObject<Paths, BaseOptions<Options>> {
13
+ protected constructor(
14
+ page: Page,
15
+ urlPath: UrlPathTypeFromOptions<BaseOptions<Options>>,
16
+ options?: { label?: string; navOptions?: NavigationOptions },
17
+ ) {
18
+ super(page, "http://localhost:8080", urlPath, options);
19
+ }
20
+ }
@@ -0,0 +1,54 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+ import dotenv from "dotenv";
3
+ import { baseConfig } from "../playwright.base";
4
+
5
+ dotenv.config({ override: false, quiet: true });
6
+
7
+ export default defineConfig({
8
+ ...baseConfig,
9
+ testDir: "./tests",
10
+
11
+ webServer: process.env.CI
12
+ ? [
13
+ {
14
+ command: "pnpm start",
15
+ url: "http://localhost:8080/",
16
+ timeout: 5 * 60_000,
17
+ reuseExistingServer: false,
18
+ ignoreHTTPSErrors: false,
19
+ },
20
+ ]
21
+ : [
22
+ {
23
+ command: "pnpm start",
24
+ url: "http://localhost:8080/",
25
+ timeout: 5 * 60_000,
26
+ reuseExistingServer: true,
27
+ ignoreHTTPSErrors: false,
28
+ },
29
+ ],
30
+
31
+ projects: [
32
+ {
33
+ name: "chromium",
34
+ use: {
35
+ ...(baseConfig.use ?? {}),
36
+ ...devices["Desktop Chrome"],
37
+ },
38
+ },
39
+ {
40
+ name: "firefox",
41
+ use: {
42
+ ...(baseConfig.use ?? {}),
43
+ ...devices["Desktop Firefox"],
44
+ },
45
+ },
46
+ {
47
+ name: "webkit",
48
+ use: {
49
+ ...(baseConfig.use ?? {}),
50
+ ...devices["Desktop Safari"],
51
+ },
52
+ },
53
+ ],
54
+ });
@@ -0,0 +1,216 @@
1
+ const express = require("express");
2
+ const path = require("node:path");
3
+
4
+ const app = express();
5
+ const port = 8080;
6
+
7
+ // Serve static files from the "test-data/staticPage" directory
8
+ app.use(express.static(path.join(__dirname, "test-data/staticPage")));
9
+
10
+ // Route to handle "/testpath/:color"
11
+ app.get("/testpath/:color", (req, res) => {
12
+ const color = req.params.color;
13
+
14
+ // Ensure the color is a valid 3 or 6-character hex code
15
+ if (!/^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(color)) {
16
+ res.status(400).send("Invalid color code.");
17
+ return;
18
+ }
19
+
20
+ // Return a simple HTML page with the background set to the color
21
+ res.send(`
22
+ <html>
23
+ <body style="background-color: #${color}; height: 100vh; margin: 0; display: flex; justify-content: center; align-items: center; flex-direction: column;">
24
+ <h1 style="border: 3px solid black; padding: 10px; background-color: #E0E0E0; color: black;">Your Random Color is:</h1>
25
+ <table role="table" aria-label="Hex Code Information" style="text-align: center; font-size: 24px; background-color: #E0E0E0; color: black; border-collapse: collapse;">
26
+ <tr role="row">
27
+ <td role="rowheader" aria-label="Hex Code Header" style="border: 3px solid black; padding: 10px; background-color: silver;">Hex Code:</td>
28
+ </tr>
29
+ <tr role="row">
30
+ <td role="cell" aria-label="color code" style="border: 3px solid black; padding: 10px;">#${color}</td>
31
+ </tr>
32
+ </table>
33
+ </body>
34
+ </html>
35
+ `);
36
+ });
37
+
38
+ // Route to handle "/testpath" with a link that generates a new random color on click
39
+ app.get("/testpath", (req, res) => {
40
+ res.send(`
41
+ <html>
42
+ <body style="display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
43
+ <a id="randomColorLink" href="#" style="font-size: 24px;" aria-label="Random Color Link">Go to Random Color Page</a>
44
+
45
+ <script>
46
+ // Function to generate a random hex color of either 3 or 6 digits
47
+ function generateRandomColor() {
48
+ // Randomly choose between generating a 3-character or 6-character hex color
49
+ const isShort = Math.random() < 0.5;
50
+ let randomColor = Math.floor(Math.random() * (isShort ? 4095 : 16777215)).toString(16);
51
+ // Pad the color if necessary to make it 3 or 6 characters long
52
+ return isShort ? randomColor.padStart(3, '0') : randomColor.padStart(6, '0');
53
+ }
54
+
55
+ // Add a click event listener to the link
56
+ document.getElementById('randomColorLink').addEventListener('click', function(event) {
57
+ event.preventDefault(); // Prevent the default action of the link
58
+ const randomColor = generateRandomColor(); // Generate a new random color
59
+ window.location.href = '/testpath/' + randomColor; // Navigate to the random color URL
60
+ });
61
+ </script>
62
+ </body>
63
+ </html>
64
+ `);
65
+ });
66
+
67
+ // Route to handle "/testfilters"
68
+ app.get("/testfilters", (req, res) => {
69
+ res.send(`
70
+ <html>
71
+ <body style="margin: 0; padding: 20px; font-family: Arial, sans-serif;">
72
+ <section id="section1" style="margin-bottom: 20px; border: 1px solid black; padding: 10px;">
73
+ <h2 style="color: silver; text-shadow: 1px 1px 1px black;">Primary Colors Section</h2>
74
+ <button style="background-color: red; color: white; padding: 10px;" onclick="changeColor('section1', 'red')">Red</button>
75
+ <button style="background-color: blue; color: white; padding: 10px;" onclick="changeColor('section1', 'blue')">Blue</button>
76
+ <button style="background-color: green; color: white; padding: 10px;" onclick="changeColor('section1', 'green')">Green</button>
77
+ <button style="background-color: lightgray; color: black; padding: 10px;" onclick="resetColor('section1')">Reset Color</button>
78
+ </section>
79
+
80
+ <section id="section2" style="margin-bottom: 20px; border: 1px solid black; padding: 10px;">
81
+ <h2 style="color: silver; text-shadow: 1px 1px 1px black;">Primary Colors Explorer</h2>
82
+ <button style="background-color: red; color: white; padding: 10px;" onclick="changeColor('section2', 'red')">Red</button>
83
+ <button style="background-color: blue; color: white; padding: 10px;" onclick="changeColor('section2', 'blue')">Blue</button>
84
+ <button style="background-color: green; color: white; padding: 10px;" onclick="changeColor('section2', 'green')">Green</button>
85
+ <button style="background-color: lightgray; color: black; padding: 10px;" onclick="resetColor('section2')">Reset Color</button>
86
+ </section>
87
+
88
+ <section id="section3" aria-label="Accessible Section" style="margin-bottom: 20px; border: 1px solid black; padding: 10px;">
89
+ <h2 style="color: silver; text-shadow: 1px 1px 1px black;">Primary Colors Test</h2>
90
+ <button style="background-color: red; color: white; padding: 10px;" onclick="changeColor('section3', 'red')">Red</button>
91
+ <button style="background-color: blue; color: white; padding: 10px;" onclick="changeColor('section3', 'blue')">Blue</button>
92
+ <button style="background-color: green; color: white; padding: 10px;" onclick="changeColor('section3', 'green')">Green</button>
93
+ <button style="background-color: lightgray; color: black; padding: 10px;" onclick="resetColor('section3')">Reset Color</button>
94
+ </section>
95
+
96
+ <section id="section4" style="margin-bottom: 20px; border: 1px solid black; padding: 10px;">
97
+ <h2 style="color: silver; text-shadow: 1px 1px 1px black;">Primary Colors Playground</h2>
98
+ <button style="background-color: red; color: white; padding: 10px;" onclick="changeColor('section4', 'red')">Red</button>
99
+ <button style="background-color: blue; color: white; padding: 10px;" onclick="changeColor('section4', 'blue')">Blue</button>
100
+ <button style="background-color: green; color: white; padding: 10px;" onclick="changeColor('section4', 'green')">Green</button>
101
+ <button style="background-color: lightgray; color: black; padding: 10px;" onclick="resetColor('section4')">Reset Color</button>
102
+ </section>
103
+
104
+ <script>
105
+ function changeColor(sectionId, color) {
106
+ document.getElementById(sectionId).style.backgroundColor = color;
107
+ }
108
+
109
+ function resetColor(sectionId) {
110
+ document.getElementById(sectionId).style.backgroundColor = "";
111
+ }
112
+ </script>
113
+ </body>
114
+ </html>
115
+ `);
116
+ });
117
+
118
+ app.get("/iframe", (_req, res) => {
119
+ res.send(`
120
+ <html>
121
+ <body style="margin: 0; padding: 20px; font-family: Arial, sans-serif;">
122
+ <h1>Iframe playground</h1>
123
+
124
+ <section id="sectionA" style="margin-bottom: 20px; border: 2px solid #333; padding: 10px;">
125
+ <h2>Section A</h2>
126
+ <iframe
127
+ id="iframeA"
128
+ title="iframeA"
129
+ src="/iframe/a"
130
+ style="width: 400px; height: 200px; border: 3px solid #0066cc;"
131
+ ></iframe>
132
+ </section>
133
+
134
+ <section id="sectionB" style="margin-bottom: 20px; border: 2px solid #333; padding: 10px;">
135
+ <h2>Section B</h2>
136
+ <iframe
137
+ id="iframeB"
138
+ title="iframeB"
139
+ src="/iframe/b"
140
+ style="width: 400px; height: 240px; border: 3px solid #cc6600;"
141
+ ></iframe>
142
+ </section>
143
+ </body>
144
+ </html>
145
+ `);
146
+ });
147
+
148
+ app.get("/iframe/a", (_req, res) => {
149
+ res.send(`
150
+ <html>
151
+ <body style="margin: 0; padding: 10px; font-family: Arial, sans-serif;">
152
+ <h3>Frame A</h3>
153
+ <button id="toggleA" data-testid="toggle-a" aria-pressed="false">Toggle A: Off</button>
154
+ <script>
155
+ const buttonA = document.getElementById('toggleA');
156
+ buttonA.addEventListener('click', () => {
157
+ const pressed = buttonA.getAttribute('aria-pressed') === 'true';
158
+ buttonA.setAttribute('aria-pressed', (!pressed).toString());
159
+ buttonA.textContent = pressed ? 'Toggle A: Off' : 'Toggle A: On';
160
+ });
161
+ </script>
162
+ </body>
163
+ </html>
164
+ `);
165
+ });
166
+
167
+ app.get("/iframe/b", (_req, res) => {
168
+ res.send(`
169
+ <html>
170
+ <body style="margin: 0; padding: 10px; font-family: Arial, sans-serif;">
171
+ <h3>Frame B</h3>
172
+ <button id="toggleB" data-testid="toggle-b" aria-pressed="false">Toggle B: Off</button>
173
+
174
+ <iframe
175
+ id="iframeC"
176
+ title="iframeC"
177
+ src="/iframe/c"
178
+ style="display: block; margin-top: 12px; width: 320px; height: 160px; border: 3px solid #009933;"
179
+ ></iframe>
180
+
181
+ <script>
182
+ const buttonB = document.getElementById('toggleB');
183
+ buttonB.addEventListener('click', () => {
184
+ const pressed = buttonB.getAttribute('aria-pressed') === 'true';
185
+ buttonB.setAttribute('aria-pressed', (!pressed).toString());
186
+ buttonB.textContent = pressed ? 'Toggle B: Off' : 'Toggle B: On';
187
+ });
188
+ </script>
189
+ </body>
190
+ </html>
191
+ `);
192
+ });
193
+
194
+ app.get("/iframe/c", (_req, res) => {
195
+ res.send(`
196
+ <html>
197
+ <body style="margin: 0; padding: 10px; font-family: Arial, sans-serif;">
198
+ <h3>Frame C</h3>
199
+ <button id="toggleC" data-testid="toggle-c" aria-pressed="false">Toggle C: Off</button>
200
+ <script>
201
+ const buttonC = document.getElementById('toggleC');
202
+ buttonC.addEventListener('click', () => {
203
+ const pressed = buttonC.getAttribute('aria-pressed') === 'true';
204
+ buttonC.setAttribute('aria-pressed', (!pressed).toString());
205
+ buttonC.textContent = pressed ? 'Toggle C: Off' : 'Toggle C: On';
206
+ });
207
+ </script>
208
+ </body>
209
+ </html>
210
+ `);
211
+ });
212
+
213
+ // Start the server
214
+ app.listen(port, () => {
215
+ console.log(`Server running at http://localhost:${port}/`);
216
+ });