Add Playwright for browser tests, with some tests for the index page

dc2ef09884258e1dba8c9673e6da879da5bc4bee

Tucker McKnight <tmcknight@instructure.com> | Sat Sep 05 2026

Add Playwright for browser tests, with some tests for the index page
.gitignore:3
Before
2
3








example_site/node_modules
example_site/_site
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
docs
After
2
3
4
5
6
7
8
9
10
11
12
example_site/node_modules
example_site/_site
docs
browser_tests/setup/node_modules
browser_tests/setup/_site

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
browser_tests/setup/eleventy.config.js:0
Before






































⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import repoViewer, {beforeHook} from "eleventy-plugin-repoviewer"
import eleventyRss from "@11ty/eleventy-plugin-rss"
import { RenderPlugin } from "@11ty/eleventy"
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"

export default async function(eleventyConfig) {
  eleventyConfig.addPlugin(eleventyRss)
  eleventyConfig.addPlugin(RenderPlugin)
  eleventyConfig.addPlugin(syntaxHighlight)

  const reposConfig = {
    baseUrl: "https://localhost:8080",
    path: '/repos',
    repos: {
      "git static site UI": {
        defaultTemplate: {
          homepageButtons: [
            {
              url: "https://example.com",
              text: "Setup Guide",
            }
          ]
        },
        defaultBranch: "main",
        tags: [],
        branches: [
          "**",
        ],
        location: "../../",
        description: "Do you like plain old HTML pages? Want to host git repos on your own site? Cool, me too.",
      },
    }
  }

  eleventyConfig.on("eleventy.beforeConfig", beforeHook(eleventyConfig, reposConfig))
  eleventyConfig.on("eleventy.beforeConfig", async () => {
    eleventyConfig.addPassthroughCopy("test.txt")
  })
  eleventyConfig.addPlugin(repoViewer, reposConfig)
};
browser_tests/setup/package-lock.json:0
Before
After
browser_tests/setup/package.json:0
Before











⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
{
  "name": "eleventy-repoviewer-example-site",
  "version": "1.0.0",
  "description": "A sample 11ty site that uses eleventy-plugin-repoviewer to display a git repository. In this case, the git repository being displayed is eleventy-plugin-repoviewer itself.",
  "license": "ISC",
  "author": "Tucker McKnight",
  "type": "module",
  "dependencies": {
    "@11ty/eleventy": "^3.1.2",
    "@11ty/eleventy-plugin-rss": "^2.0.4",
    "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
    "eleventy-plugin-repoviewer": "file:../../"
  }
}
browser_tests/tests/index.spec.ts:0
Before




































⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  await page.goto('http://localhost:8081/repos');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle("Repositories");
});

test('has links to repos and shows clone URL', async ({ page }) => {
  await page.goto('http://localhost:8081/repos');

  await expect(
    page.getByText('https://localhost:8080/repos/git-static-site-ui.git')
  ).toBeVisible();

  // Go to the first repo on the page
  await page.getByRole('link', { name: 'Go to site' }).click();

  // Expects page to have a heading with the name of Installation.
  await expect(page.getByRole('heading', { name: 'git static site UI' })).toBeVisible();
});

test('the light mode/dark mode switch works', async ({ page }) => {
  await page.goto('http://localhost:8081/repos');

  // click light mode
  (await page.waitForSelector("#dark-mode-switch")).click();
  await page.getByRole('button', { name: /Light/ }).click();

  const body =  await page.locator('body');
  expect(body).toHaveCSS('background-color', "rgb(239, 242, 228)");

  // click dark mode
  (await page.waitForSelector("#dark-mode-switch")).click();
  await page.getByRole('button', { name: /Dark/ }).click();

  expect(body).toHaveCSS('background-color', "rgb(8, 15, 53)");
});
package-lock.json:18
Before
17
18
19

20
21
        "mithril-node-render": "^3.0.2"
      },
      "devDependencies": {
⁣
        "@types/node": "^24.0.7",
        "bootstrap": "^5.3.8",
        "rollup": "^4.57.1",
After
17
18
19
20
21
22
        "mithril-node-render": "^3.0.2"
      },
      "devDependencies": {
        "@playwright/test": "^1.63.0",
        "@types/node": "^24.0.7",
        "bootstrap": "^5.3.8",
        "rollup": "^4.57.1",
package-lock.json:431
Before
430
431
432
















433
434
        "url": "https://opencollective.com/parcel"
      }
    },
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
    "node_modules/@popperjs/core": {
      "version": "2.11.8",
      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
After
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/@playwright/test": {
      "version": "1.63.0",
      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
      "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "playwright": "1.63.0"
      },
      "bin": {
        "playwright": "cli.js"
      },
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/@popperjs/core": {
      "version": "2.11.8",
      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
package-lock.json:552
Before
551
552
553
554
555
556
557
558
        "arm64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
551
552
553



554
555
        "arm64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:572
Before
571
572
573
574
575
576
577
578
        "arm64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
571
572
573



574
575
        "arm64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:592
Before
591
592
593
594
595
596
597
598
        "ppc64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
591
592
593



594
595
        "ppc64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:612
Before
611
612
613
614
615
616
617
618
        "s390x"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
611
612
613



614
615
        "s390x"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:632
Before
631
632
633
634
635
636
637
638
        "x64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
631
632
633



634
635
        "x64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:652
Before
651
652
653
654
655
656
657
658
        "x64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
After
651
652
653



654
655
        "x64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MIT",
      "optional": true,
      "os": [
package-lock.json:1923
Before
1922
1923
1924
1925
1926
1927
1928
1929
        "arm64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
After
1922
1923
1924



1925
1926
        "arm64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MPL-2.0",
      "optional": true,
      "os": [
package-lock.json:1947
Before
1946
1947
1948
1949
1950
1951
1952
1953
        "arm64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
After
1946
1947
1948



1949
1950
        "arm64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MPL-2.0",
      "optional": true,
      "os": [
package-lock.json:1971
Before
1970
1971
1972
1973
1974
1975
1976
1977
        "x64"
      ],
      "dev": true,
      "libc": [
        "glibc"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
After
1970
1971
1972



1973
1974
        "x64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MPL-2.0",
      "optional": true,
      "os": [
package-lock.json:1995
Before
1994
1995
1996
1997
1998
1999
2000
2001
        "x64"
      ],
      "dev": true,
      "libc": [
        "musl"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
After
1994
1995
1996



1997
1998
        "x64"
      ],
      "dev": true,
⁣
⁣
⁣
      "license": "MPL-2.0",
      "optional": true,
      "os": [
package-lock.json:2280
Before
2279
2280
2281





























2282
2283
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
    "node_modules/postcss": {
      "version": "8.5.26",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
After
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/playwright": {
      "version": "1.63.0",
      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
      "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "playwright-core": "1.63.0"
      },
      "bin": {
        "playwright": "cli.js"
      },
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/playwright-core": {
      "version": "1.63.0",
      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
      "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "playwright-core": "cli.js"
      },
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/postcss": {
      "version": "8.5.26",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
package.json:18
Before
17
18
19

20
21
  "license": "ISC",
  "description": "",
  "devDependencies": {
⁣
    "@types/node": "^24.0.7",
    "bootstrap": "^5.3.8",
    "rollup": "^4.57.1",
After
17
18
19
20
21
22
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@playwright/test": "^1.63.0",
    "@types/node": "^24.0.7",
    "bootstrap": "^5.3.8",
    "rollup": "^4.57.1",
playwright.config.ts:0
Before












































































⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { defineConfig, devices } from '@playwright/test';

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: './browser_tests/tests',
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('')`. */
    // baseURL: 'http://localhost:3000',

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry',
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },

    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
    // },
  ],

  /* Run your local dev server before starting the tests */
  // webServer: {
  //   command: 'npm run start',
  //   url: 'http://localhost:3000',
  //   reuseExistingServer: !process.env.CI,
  // },
});
tsconfig.json:7
Before
6
7
8
9
10
11
12
13
14
15

16
    "module": "node18",
    "target": "es6",
    "lib": ["esnext", "dom"],
    "allowJs": true,
    "outDir": "dist"
  },
  "include": ["**/*.ts"],
  "exclude": [
    "dist/**/*",
    "ts/frontend/**/*",
⁣
  ],
}
After
6
7
8

9
10
11
12
13
14
15
16
    "module": "node18",
    "target": "es6",
    "lib": ["esnext", "dom"],
⁣
    "outDir": "dist"
  },
  "include": ["**/*.ts"],
  "exclude": [
    "dist/**/*",
    "ts/frontend/**/*",
    "browser_tests/**/*"
  ],
}