diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c228220..0412ba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,8 @@ jobs: - name: Install dependencies run: yarn --immutable + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true - name: Build package run: yarn build @@ -91,6 +93,15 @@ jobs: restore-keys: | ${{ runner.os }}-${{ env.cache-name }} + - name: Cache ~/.cache/ms-playwright + id: playwright-cache + uses: actions/cache@v4 + env: + cache-name: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} + - name: Use Node.js uses: actions/setup-node@v4 with: @@ -102,5 +113,9 @@ jobs: - name: Install dependencies run: yarn --immutable + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: yarn workspace react-clock playwright install chromium-headless-shell + - name: Run tests run: yarn unit diff --git a/.gitignore b/.gitignore index 51c156e..3e3740f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ !**/.yarn/versions # Project-generated directories and files +__screenshots__ coverage dist node_modules diff --git a/.yarnrc.yml b/.yarnrc.yml index 84eab6b..5255367 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,3 +1,5 @@ +enableScripts: false + logFilters: - code: YN0076 level: discard diff --git a/packages/react-clock/package.json b/packages/react-clock/package.json index 0a2e620..db6b1a6 100644 --- a/packages/react-clock/package.json +++ b/packages/react-clock/package.json @@ -57,12 +57,14 @@ "@types/node": "*", "@types/react": "*", "@types/react-dom": "*", + "@vitest/browser": "^3.2.3", "cpy-cli": "^5.0.0", - "happy-dom": "^15.10.2", + "playwright": "^1.51.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.5.2", - "vitest": "^3.2.3" + "vitest": "^3.2.3", + "vitest-browser-react": "^1.0.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/packages/react-clock/src/Clock.spec.tsx b/packages/react-clock/src/Clock.spec.tsx index a767649..15566ba 100644 --- a/packages/react-clock/src/Clock.spec.tsx +++ b/packages/react-clock/src/Clock.spec.tsx @@ -150,19 +150,32 @@ describe('Clock', () => { const secondMillisecondAngle = secondAngle / 1000; function getDeg(transform: string) { - const match = transform.match(/rotate\(([0-9.]*)deg\)/); + const match = transform.match( + /matrix\((-?[\d.]+), (-?[\d.]+), (-?[\d.]+), (-?[\d.]+), (-?[\d.]+), (-?[\d.]+)\)/, + ); if (!match) { throw new Error('Could not parse transform'); } - const deg = match[1]; + const [_, ...rawNumbers] = match; - if (!deg) { + const numbers = rawNumbers.map(Number.parseFloat); + + const a = numbers[0]; + const b = numbers[1]; + + if (typeof a !== 'number' || typeof b !== 'number') { throw new Error('Could not parse transform'); } - return Number.parseFloat(deg); + const radians = Math.atan2(b, a); + const degrees = radians * (180 / Math.PI); + + // Normalize to [0, 360) + const normalized = ((degrees % 360) + 360) % 360; + + return normalized; } function getAngle(hand: HTMLElement) { diff --git a/packages/react-clock/src/Hand.spec.tsx b/packages/react-clock/src/Hand.spec.tsx index e6c96ee..ad14e48 100644 --- a/packages/react-clock/src/Hand.spec.tsx +++ b/packages/react-clock/src/Hand.spec.tsx @@ -19,7 +19,7 @@ describe('Hand', () => { const hand = container.querySelector('.react-clock__hand'); - expect(hand).toHaveStyle('transform: rotate(0deg)'); + expect(hand).toHaveStyle('transform: matrix(1, 0, 0, 1, 0, 0)'); // rotate(0deg) }); it('renders properly angled hand given angle prop', () => { @@ -27,7 +27,7 @@ describe('Hand', () => { const hand = container.querySelector('.react-clock__hand'); - expect(hand).toHaveStyle('transform: rotate(15deg)'); + expect(hand).toHaveStyle('transform: matrix(0.965926, 0.258819, -0.258819, 0.965926, 0, 0)'); // rotate(15deg) }); it('renders hand with 100% length by default', () => { diff --git a/packages/react-clock/src/Mark.spec.tsx b/packages/react-clock/src/Mark.spec.tsx index e481300..5942cf5 100644 --- a/packages/react-clock/src/Mark.spec.tsx +++ b/packages/react-clock/src/Mark.spec.tsx @@ -19,7 +19,7 @@ describe('Mark', () => { const mark = container.querySelector('.react-clock__mark'); - expect(mark).toHaveStyle('transform: rotate(0deg)'); + expect(mark).toHaveStyle('transform: matrix(1, 0, 0, 1, 0, 0)'); // rotate(0deg) }); it('renders properly angled mark given angle prop', () => { @@ -27,7 +27,7 @@ describe('Mark', () => { const mark = container.querySelector('.react-clock__mark'); - expect(mark).toHaveStyle('transform: rotate(15deg)'); + expect(mark).toHaveStyle('transform: matrix(0.965926, 0.258819, -0.258819, 0.965926, 0, 0)'); // rotate(15deg) }); it('renders mark with 10% length by default', () => { diff --git a/packages/react-clock/src/MarkNumber.spec.tsx b/packages/react-clock/src/MarkNumber.spec.tsx index f90257f..ca7a1f7 100644 --- a/packages/react-clock/src/MarkNumber.spec.tsx +++ b/packages/react-clock/src/MarkNumber.spec.tsx @@ -17,7 +17,7 @@ describe('MarkNumber', () => { const markNumber = container.querySelector('.react-clock__mark__number'); - expect(markNumber).toHaveStyle('transform: rotate(-0deg)'); + expect(markNumber).toHaveStyle('transform: matrix(1, 0, 0, 1, 0, 0)'); // rotate(0deg) }); it('renders properly angled mark given angle prop', () => { @@ -25,6 +25,8 @@ describe('MarkNumber', () => { const markNumber = container.querySelector('.react-clock__mark__number'); - expect(markNumber).toHaveStyle('transform: rotate(-15deg)'); + expect(markNumber).toHaveStyle( + 'transform: matrix(0.965926, -0.258819, 0.258819, 0.965926, 0, 0)', + ); // rotate(-15deg) }); }); diff --git a/packages/react-clock/tsconfig.json b/packages/react-clock/tsconfig.json index 72f7513..edb2c00 100644 --- a/packages/react-clock/tsconfig.json +++ b/packages/react-clock/tsconfig.json @@ -13,6 +13,7 @@ "skipLibCheck": true, "strict": true, "target": "es2018", + "types": ["@vitest/browser/matchers"], "verbatimModuleSyntax": true }, "exclude": ["dist"] diff --git a/packages/react-clock/vitest.config.ts b/packages/react-clock/vitest.config.ts index b61c66c..5916977 100644 --- a/packages/react-clock/vitest.config.ts +++ b/packages/react-clock/vitest.config.ts @@ -2,7 +2,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - environment: 'happy-dom', + browser: { + enabled: true, + headless: true, + instances: [{ browser: 'chromium' }], + provider: 'playwright', + }, setupFiles: 'vitest.setup.ts', watch: false, }, diff --git a/yarn.lock b/yarn.lock index f7a849f..09c7b1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -613,6 +613,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.19": version: 1.0.0-beta.19 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.19" @@ -760,19 +767,19 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^10.0.0": - version: 10.0.0 - resolution: "@testing-library/dom@npm:10.0.0" +"@testing-library/dom@npm:^10.0.0, @testing-library/dom@npm:^10.4.0": + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1" dependencies: "@babel/code-frame": "npm:^7.10.4" "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^5.0.1" aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" dom-accessibility-api: "npm:^0.5.9" lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" pretty-format: "npm:^27.0.2" - checksum: 10c0/2d12d2a6018a6f1d15e91834180bc068932c699ff1fcbfb80aa21aba519a4f5329c861dfa852e06ee5615bcb92ef2a0f0e755e32684ea3dada63bc34248382ab + checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1 languageName: node linkType: hard @@ -826,6 +833,15 @@ __metadata: languageName: node linkType: hard +"@testing-library/user-event@npm:^14.6.1": + version: 14.6.1 + resolution: "@testing-library/user-event@npm:14.6.1" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10c0/75fea130a52bf320d35d46ed54f3eec77e71a56911b8b69a3fe29497b0b9947b2dc80d30f04054ad4ce7f577856ae3e5397ea7dff0ef14944d3909784c7a93fe + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.1 resolution: "@types/aria-query@npm:5.0.1" @@ -956,6 +972,33 @@ __metadata: languageName: node linkType: hard +"@vitest/browser@npm:^3.2.3": + version: 3.2.3 + resolution: "@vitest/browser@npm:3.2.3" + dependencies: + "@testing-library/dom": "npm:^10.4.0" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/mocker": "npm:3.2.3" + "@vitest/utils": "npm:3.2.3" + magic-string: "npm:^0.30.17" + sirv: "npm:^3.0.1" + tinyrainbow: "npm:^2.0.0" + ws: "npm:^8.18.2" + peerDependencies: + playwright: "*" + vitest: 3.2.3 + webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + checksum: 10c0/effc8affe57ab4bfe12cc71dc5506c85b034b018c1fc71fde8591be77755962f979e06aae23b7c80460d93b36f3106ab52611d839d2d45be08450e81fd80c8ee + languageName: node + linkType: hard + "@vitest/expect@npm:3.2.3": version: 3.2.3 resolution: "@vitest/expect@npm:3.2.3" @@ -1239,16 +1282,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.1.0": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 - languageName: node - linkType: hard - "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -1445,13 +1478,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.5.0": - version: 4.5.0 - resolution: "entities@npm:4.5.0" - checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -1658,6 +1684,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -1668,6 +1704,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -1744,17 +1789,6 @@ __metadata: languageName: node linkType: hard -"happy-dom@npm:^15.10.2": - version: 15.10.2 - resolution: "happy-dom@npm:15.10.2" - dependencies: - entities: "npm:^4.5.0" - webidl-conversions: "npm:^7.0.0" - whatwg-mimetype: "npm:^3.0.0" - checksum: 10c0/b0403c4c53021da25989b320f2a6c0ab760cc538f10e403df9d2f14b0a7d20b1be961192c95322b335dc71fa27941e7e8b883b2d79d7c9c51215a97b3e3097a6 - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -2167,6 +2201,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -2321,7 +2362,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": +"picocolors@npm:1.1.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -2342,6 +2383,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.54.2": + version: 1.54.2 + resolution: "playwright-core@npm:1.54.2" + bin: + playwright-core: cli.js + checksum: 10c0/44850e20bf35237c8c3dedf1096c655f8af939dde53c5469f72cae3dd744966858a302419b909a73d7a2093323123e7ebcc0fdd55151b4193afb7812c1fd2c88 + languageName: node + linkType: hard + +"playwright@npm:^1.51.1": + version: 1.54.2 + resolution: "playwright@npm:1.54.2" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.54.2" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/6f642fa70179eee5d5bf8a90df2a6147c9638ff926f4f3ad0a0517396b8a3fe00ccebf13377e032a75b3f0b2610ec1562293e0cfc3bde234181c7a50af8af80a + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -2407,15 +2472,17 @@ __metadata: "@types/node": "npm:*" "@types/react": "npm:*" "@types/react-dom": "npm:*" + "@vitest/browser": "npm:^3.2.3" "@wojtekmaj/date-utils": "npm:^2.0.2" clsx: "npm:^2.0.0" cpy-cli: "npm:^5.0.0" get-user-locale: "npm:^3.0.0" - happy-dom: "npm:^15.10.2" + playwright: "npm:^1.51.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" typescript: "npm:^5.5.2" vitest: "npm:^3.2.3" + vitest-browser-react: "npm:^1.0.1" peerDependencies: "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2633,6 +2700,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.1": + version: 3.0.1 + resolution: "sirv@npm:3.0.1" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/7cf64b28daa69b15f77b38b0efdd02c007b72bb3ec5f107b208ebf59f01b174ef63a1db3aca16d2df925501831f4c209be6ece3302b98765919ef5088b45bf80 + languageName: node + linkType: hard + "slash@npm:^4.0.0": version: 4.0.0 resolution: "slash@npm:4.0.0" @@ -2857,6 +2935,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + "typescript@npm:^5.5.2": version: 5.5.2 resolution: "typescript@npm:5.5.2" @@ -2986,6 +3071,25 @@ __metadata: languageName: node linkType: hard +"vitest-browser-react@npm:^1.0.1": + version: 1.0.1 + resolution: "vitest-browser-react@npm:1.0.1" + peerDependencies: + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + "@vitest/browser": ^2.1.0 || ^3.0.0 || ^4.0.0-0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + vitest: ^2.1.0 || ^3.0.0 || ^4.0.0-0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/a83353743cb490df846cca81d6a7661568088e079c174e62aa561b34db032e7e042770cb7557d25ab2c66c959d303b7a4706ba49f2b715796823b663ef469e41 + languageName: node + linkType: hard + "vitest@npm:^3.2.3": version: 3.2.3 resolution: "vitest@npm:3.2.3" @@ -3042,20 +3146,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^7.0.0": - version: 7.0.0 - resolution: "webidl-conversions@npm:7.0.0" - checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 - languageName: node - linkType: hard - -"whatwg-mimetype@npm:^3.0.0": - version: 3.0.0 - resolution: "whatwg-mimetype@npm:3.0.0" - checksum: 10c0/323895a1cda29a5fb0b9ca82831d2c316309fede0365047c4c323073e3239067a304a09a1f4b123b9532641ab604203f33a1403b5ca6a62ef405bcd7a204080f - languageName: node - linkType: hard - "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -3112,6 +3202,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.2": + version: 8.18.3 + resolution: "ws@npm:8.18.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1"