Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/testdriver.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: TestDriver.ai Tests

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
permissions:
id-token: write # required to mint the OIDC token for auth
contents: read

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

# Authenticate to TestDriver via GitHub OIDC (no stored secret needed once the
# TestDriver GitHub App is authorized for your org). Falls back to a TD_API_KEY
# secret if you set one. Exports TD_API_KEY for the test step below.
- name: Authenticate to TestDriver
uses: testdriverai/action@stable
with:
api-key: ${{ secrets.TD_API_KEY }}

- name: Run TestDriver.ai tests
run: npx vitest run

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
retention-days: 30
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules/
.env
tmp/
.testdriver/
test-results/
.claude/settings.local.json
.claude/worktrees/
.claude/scheduled_tasks.lock
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"vitest.explorer"
]
}
41 changes: 41 additions & 0 deletions tests/birdsbirdbirds.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";

describe("BirdsBirdBirds Challenge", () => {
it("allows selecting drawing tools and saving a bird", async (context) => {
const testdriver = TestDriver(context);

await testdriver.provision.chrome({
url: process.env.APP_URL || "https://arcade-quarter-horn-boxing.trycloudflare.com",
});

const assertHeader = await testdriver.assert(
"the Draw your bird header and drawing tools are visible"
);
expect(assertHeader).toBeTruthy();

const saveButton = await testdriver.find("Save my bird button");
await saveButton.click();

await testdriver.scroll("down", { amount: 200 });

const assertEmpty = await testdriver.assert(
'the message "The canvas is empty. Draw your bird first!" is visible'
);
expect(assertEmpty).toBeTruthy();

await testdriver.scroll("up", { amount: 200 });

const eraserButton = await testdriver.find("Eraser button");
await eraserButton.click();

const assertEraser = await testdriver.assert("the Eraser tool is selected");
expect(assertEraser).toBeTruthy();

const penButton = await testdriver.find("Pen button");
await penButton.click();

const assertPen = await testdriver.assert("the Pen tool is selected");
expect(assertPen).toBeTruthy();
});
});
53 changes: 53 additions & 0 deletions tools/serve-birds.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Serves the birdsbirdbirds challenge page on the requested port with Host header forwarding.
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import http from 'node:http'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const setupHook = path.join(repoRoot, 'challenges', 'birdsbirdbirds', 'setup')
const cleanupHook = path.join(repoRoot, 'challenges', 'birdsbirdbirds', 'cleanup')

const port = Number(process.env.PORT || 3000)
const internalPort = 4322
const runDir = path.join('/tmp', `birds-serve-${Date.now()}`)

fs.mkdirSync(runDir, { recursive: true })

const setup = spawn(setupHook, [], { cwd: runDir, stdio: 'inherit' })
setup.on('exit', (code) => {
if (code !== 0) {
console.error(`setup failed with code ${code}`)
process.exit(code ?? 1)
}

const proxy = http.createServer((req, res) => {
const options = {
hostname: '127.0.0.1',
port: internalPort,
path: req.url,
method: req.method,
headers: { ...req.headers, host: `127.0.0.1:${internalPort}` },
}
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.pipe(res, { end: true })
})
req.pipe(proxyReq, { end: true })
})

proxy.listen(port, '0.0.0.0', () => {
console.log(`BirdsBirdBirds server listening on port ${port}`)
})

function cleanup() {
try {
spawn(cleanupHook, [], { cwd: runDir, stdio: 'ignore' })
} catch {}
process.exit(0)
}

process.on('SIGTERM', cleanup)
process.on('SIGINT', cleanup)
})
16 changes: 16 additions & 0 deletions vitest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config';
import TestDriver from 'testdriverai/vitest';

// Note: dotenv is loaded automatically by the TestDriver SDK
export default defineConfig({
test: {
include: ['tests/**/*.test.{js,mjs,ts}'],
testTimeout: 300000,
hookTimeout: 300000,
reporters: [
'default',
TestDriver(),
],
setupFiles: ['testdriverai/vitest/setup'],
},
});
Loading