feat: add Berlin Clock e2e tests

This commit is contained in:
Loic Coenen
2026-06-23 15:34:55 +02:00
parent 0965f585d0
commit 5a6de57a91
12 changed files with 304 additions and 42 deletions

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.aider*

View File

@@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config
```js
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
@@ -34,42 +34,40 @@ export default defineConfig([
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
]);
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
]);
```

9
cypress.config.js Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:5173",
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
supportFile: false,
},
});

212
cypress/e2e/clock.cy.ts Normal file
View File

@@ -0,0 +1,212 @@
/// <reference types="cypress" />
describe("Berlin Clock", () => {
const range = Cypress._.range;
beforeEach(() => {
cy.visit("/");
});
const enterTime = (time: string) => {
cy.get('[data-testid="time-input"]').clear().type(time).type("{enter}");
};
const getLamp = (rowTestId: string, index: number) => {
return cy.get(`[data-testid="${rowTestId}"] [data-testid="lamp-${index}"]`);
};
const assertLampColors = (rowTestId: string, expectedColors: string) => {
range(expectedColors.length).forEach((i) => {
getLamp(rowTestId, i).should(
"have.attr",
"aria-label",
expectedColors[i],
);
});
};
describe("Seconds Lamp", () => {
it("should show Y at 00:00:00", () => {
enterTime("00:00:00");
cy.get('[data-testid="seconds-lamp"]').should(
"have.attr",
"aria-label",
"Y",
);
});
it("should show O at 23:59:59", () => {
enterTime("23:59:59");
cy.get('[data-testid="seconds-lamp"]').should(
"have.attr",
"aria-label",
"O",
);
});
});
describe("Five Hours Row", () => {
it("should show OOOO at 00:00:00", () => {
enterTime("00:00:00");
assertLampColors("five-hours-row", "OOOO");
});
it("should show RRRR at 23:59:59", () => {
enterTime("23:59:59");
assertLampColors("five-hours-row", "RRRR");
});
it("should show OOOO at 02:04:00", () => {
enterTime("02:04:00");
assertLampColors("five-hours-row", "OOOO");
});
it("should show ROOO at 08:23:00", () => {
enterTime("08:23:00");
assertLampColors("five-hours-row", "ROOO");
});
it("should show RRRO at 16:35:00", () => {
enterTime("16:35:00");
assertLampColors("five-hours-row", "RRRO");
});
});
describe("Single Hours Row", () => {
it("should show OOOO at 00:00:00", () => {
enterTime("00:00:00");
assertLampColors("single-hours-row", "OOOO");
});
it("should show RRRO at 23:59:59", () => {
enterTime("23:59:59");
assertLampColors("single-hours-row", "RRRO");
});
it("should show RROO at 02:04:00", () => {
enterTime("02:04:00");
assertLampColors("single-hours-row", "RROO");
});
it("should show RRRO at 08:23:00", () => {
enterTime("08:23:00");
assertLampColors("single-hours-row", "RRRO");
});
it("should show RRRR at 14:35:00", () => {
enterTime("14:35:00");
assertLampColors("single-hours-row", "RRRR");
});
});
describe("Five Minutes Row", () => {
it("should show OOOOOOOOOOO at 00:00:00", () => {
enterTime("00:00:00");
assertLampColors("five-minutes-row", "OOOOOOOOOOO");
});
it("should show YYRYYRYYRYY at 23:59:59", () => {
enterTime("23:59:59");
assertLampColors("five-minutes-row", "YYRYYRYYRYY");
});
it("should show OOOOOOOOOOO at 12:04:00", () => {
enterTime("12:04:00");
assertLampColors("five-minutes-row", "OOOOOOOOOOO");
});
it("should show YYRYOOOOOOO at 12:23:00", () => {
enterTime("12:23:00");
assertLampColors("five-minutes-row", "YYRYOOOOOOO");
});
it("should show YYRYYRYOOOO at 12:35:00", () => {
enterTime("12:35:00");
assertLampColors("five-minutes-row", "YYRYYRYOOOO");
});
});
describe("Single Minutes Row", () => {
it("should show OOOO at 00:00:00", () => {
enterTime("00:00:00");
assertLampColors("single-minutes-row", "OOOO");
});
it("should show YYYY at 23:59:59", () => {
enterTime("23:59:59");
assertLampColors("single-minutes-row", "YYYY");
});
it("should show YYOO at 12:32:00", () => {
enterTime("12:32:00");
assertLampColors("single-minutes-row", "YYOO");
});
it("should show YYYY at 12:34:00", () => {
enterTime("12:34:00");
assertLampColors("single-minutes-row", "YYYY");
});
it("should show OOOO at 12:35:00", () => {
enterTime("12:35:00");
assertLampColors("single-minutes-row", "OOOO");
});
});
describe("Full Clock Integration", () => {
const assertFullClock = (time: string, expected: string) => {
enterTime(time);
// Seconds
cy.get('[data-testid="seconds-lamp"]').should(
"have.attr",
"aria-label",
expected[0],
);
// Five hours (indices 1-4)
range(4).forEach((i) => {
cy.get(
`[data-testid="five-hours-row"] [data-testid="lamp-${i}"]`,
).should("have.attr", "aria-label", expected[1 + i]);
});
// Single hours (indices 5-8)
range(4).forEach((i) => {
cy.get(
`[data-testid="single-hours-row"] [data-testid="lamp-${i}"]`,
).should("have.attr", "aria-label", expected[5 + i]);
});
// Five minutes (indices 9-19)
range(11).forEach((i) => {
cy.get(
`[data-testid="five-minutes-row"] [data-testid="lamp-${i}"]`,
).should("have.attr", "aria-label", expected[9 + i]);
});
// Single minutes (indices 20-23)
range(4).forEach((i) => {
cy.get(
`[data-testid="single-minutes-row"] [data-testid="lamp-${i}"]`,
).should("have.attr", "aria-label", expected[20 + i]);
});
};
it("displays correct clock for 00:00:00", () => {
assertFullClock("00:00:00", "YOOOOOOOOOOOOOOOOOOOOOOO");
});
it("displays correct clock for 23:59:59", () => {
assertFullClock("23:59:59", "ORRRRRRROYYRYYRYYRYYYYYY");
});
it("displays correct clock for 16:50:06", () => {
assertFullClock("16:50:06", "YRRROROOOYYRYYRYYRYOOOOO");
});
it("displays correct clock for 11:37:01", () => {
assertFullClock("11:37:01", "ORROOROOOYYRYYRYOOOOYYOO");
});
});
});

View File

@@ -1,14 +1,14 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
@@ -19,4 +19,4 @@ export default defineConfig([
globals: globals.browser,
},
},
])
]);

17
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.6.0",
"prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.61.0",
"vite": "^8.1.0",
@@ -5025,6 +5026,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",

View File

@@ -28,6 +28,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.6.0",
"prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.61.0",
"vite": "^8.1.0",

View File

@@ -167,7 +167,7 @@
&::before,
&::after {
content: '';
content: "";
position: absolute;
top: -4.5px;
border: 5px solid transparent;

View File

@@ -1,11 +1,11 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "./assets/vite.svg";
import heroImg from "./assets/hero.png";
import "./App.css";
function App() {
const [count, setCount] = useState(0)
const [count, setCount] = useState(0);
return (
<>
@@ -116,7 +116,7 @@ function App() {
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
);
}
export default App
export default App;

View File

@@ -11,8 +11,8 @@
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--sans: system-ui, "Segoe UI", Roboto, sans-serif;
--heading: system-ui, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);

View File

@@ -1,10 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
createRoot(document.getElementById('root')!).render(
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
)
);

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
});