diff --git a/eslint.config.js b/eslint.config.js index aa743b2..109b495 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -159,7 +159,6 @@ const vitestRules = { 'vitest/no-focused-tests': 'error', 'vitest/no-disabled-tests': 'error', 'vitest/prefer-to-contain': 'error', - 'vitest/consistent-test-it': 'error', 'vitest/prefer-called-with': 'error', 'vitest/prefer-to-be-falsy': 'error', 'vitest/no-duplicate-hooks': 'error', @@ -176,12 +175,12 @@ const vitestRules = { 'vitest/consistent-test-filename': 'error', 'vitest/no-test-return-statement': 'error', 'vitest/require-to-throw-message': 'error', - 'vitest/require-top-level-describe': 'error', 'vitest/prefer-comparison-matcher': 'error', 'vitest/no-interpolation-in-snapshots': 'error', 'vitest/prefer-mock-promise-shorthand': 'error', 'vitest/prefer-snapshot-hint': ['error', 'always'], 'vitest/max-nested-describe': ['error', { max: 1 }], + 'vitest/consistent-test-it': ['error', { fn: 'test', withinDescribe: 'test' }], }; const cypressRules = { @@ -307,25 +306,10 @@ export default eslintAntfuConfig( { files: ['tests/unit/**/*.test.ts'], plugins: { eslintPluginVitest }, - settings: { - vitest: { - typecheck: true, - }, - }, - languageOptions: { - globals: { - ...eslintPluginVitest.environments.env.globals, - }, - }, - rules: { - ...vitestRules, - 'ts/no-unsafe-call': 'off', - 'no-magic-numbers': 'off', - 'ts/no-unsafe-member-access': 'off', - }, + rules: { ...vitestRules, 'no-magic-numbers': 'off' }, }, { - files: ['tests/cypress/**/*.ts'], + files: ['tests/cypress/e2e/**/*.cy.ts'], ...eslintPluginCypress.configs.recommended, rules: { ...cypressRules, 'no-magic-numbers': 'off' }, }, diff --git a/package.json b/package.json index eb1331a..8bd6eb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@8.15.4", + "packageManager": "pnpm@9.7.1", "author": { "name": "Shayan Zamani", "url": "https://shayan-zamani.me/", @@ -41,7 +41,7 @@ "astro": "^4.4.15", "browserslist": "^4.23.0", "concurrently": "^8.2.2", - "cypress": "^13.13.2", + "cypress": "^13.13.3", "cypress-vite": "^1.5.0", "eslint": "^9.8.0", "eslint-plugin-cypress": "^3.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e191d2..7cde9a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ importers: specifier: ^8.2.2 version: 8.2.2 cypress: - specifier: ^13.13.2 - version: 13.13.2 + specifier: ^13.13.3 + version: 13.13.3 cypress-vite: specifier: ^1.5.0 version: 1.5.0(vite@5.3.5(@types/node@22.1.0)(lightningcss@1.25.1)(terser@5.31.3)) @@ -1357,8 +1357,8 @@ packages: peerDependencies: vite: ^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - cypress@13.13.2: - resolution: {integrity: sha512-PvJQU33933NvS1StfzEb8/mu2kMy4dABwCF+yd5Bi7Qly1HOVf+Bufrygee/tlmty/6j5lX+KIi8j9Q3JUMbhA==} + cypress@13.13.3: + resolution: {integrity: sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -5045,7 +5045,7 @@ snapshots: transitivePeerDependencies: - supports-color - cypress@13.13.2: + cypress@13.13.3: dependencies: '@cypress/request': 3.0.1 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) diff --git a/tests/cypress/e2e/image-editor.cy.ts b/tests/cypress/e2e/image-editor.cy.ts index 88ac458..5f27e55 100644 --- a/tests/cypress/e2e/image-editor.cy.ts +++ b/tests/cypress/e2e/image-editor.cy.ts @@ -1,9 +1,13 @@ import { activeFilterClass, imgDownloadTimeoutMS } from '@ts/constants.ts'; -before(() => { +const FIXTURES_PATH = 'tests/cypress/fixtures'; +const DOWNLOADS_PATH = 'tests/cypress/downloads'; +const INITIAL_CSS_MATRIX = 'matrix(1, 0, 0, 1, 0, 0)'; +const INITIAL_CSS_FILTERS = 'brightness(1) grayscale(0) blur(0px) hue-rotate(0deg) opacity(1) contrast(1) saturate(1) sepia(0)'; + +beforeEach(() => { cy.visit('/'); - cy.get('[data-test="rotate_left"]').as('rotateLeftBtn'); cy.get('[data-test="vertical_flip"]').as('verticalFlipBtn'); cy.get('[data-test="rotate_right"]').as('rotateRightBtn'); cy.get('[data-test="grayscale"]').as('grayscaleFilterBtn'); @@ -11,54 +15,57 @@ before(() => { cy.get('#filters_container').children().first().as('firstFilterBtn'); }); -it('goes through the process of uploading, editing, and downloading multiple images', () => { - /* Initial state */ +it('initial UI state', () => { cy.get('#edit_options_container').should('be.disabled'); cy.get('#img_save_anchor').should('have.attr', 'aria-disabled', 'true'); +}); - /* Upload an image */ - cy.get('@imgSelectLabel').selectFile('tests/cypress/fixtures/sh.png').trigger('cancel'); - cy.get('@imgSelectLabel').selectFile('tests/cypress/fixtures/pickle-rick.webp'); +it('upload, edit, and download multiple images', () => { + /* Upload an image. */ + cy.get('@imgSelectLabel').selectFile(`${FIXTURES_PATH}/pickle-rick.webp`).trigger('cancel'); + cy.get('@imgSelectLabel').selectFile(`${FIXTURES_PATH}/pickle-rick.webp`); cy.get('#edit_options_container').should('be.enabled'); cy.get('#reset_filters_btn').should('be.disabled'); cy.get('#img_save_anchor').should('have.attr', 'aria-disabled', 'true'); cy.get('#img').should('have.attr', 'alt', 'pickle-rick.webp').and('have.attr', 'title', 'pickle-rick.webp'); - /* Edit the uploaded image */ + /* Edit the uploaded image. */ + cy.get('@verticalFlipBtn').click(); + cy.get('@rotateRightBtn').click(); cy.get('@grayscaleFilterBtn').click(); - cy.get('@firstFilterBtn').should('not.have.class', activeFilterClass); - cy.get('@grayscaleFilterBtn').should('have.class', activeFilterClass); cy.get('#active_filter_range_input').invoke('val', 100); cy.get('#active_filter_range_input').trigger('input'); - cy.get('#img').should('have.css', 'filter', 'brightness(1) grayscale(1) blur(0px) hue-rotate(0deg) opacity(1) contrast(1) saturate(1) sepia(0)'); cy.get('#reset_filters_btn').should('be.enabled'); cy.get('#img_save_anchor').should('have.attr', 'aria-disabled', 'false'); - cy.get('@rotateRightBtn').click(); - cy.get('@verticalFlipBtn').click(); + cy.get('@firstFilterBtn').should('not.have.class', activeFilterClass); + cy.get('@grayscaleFilterBtn').should('have.class', activeFilterClass); cy.get('#img').should('have.css', 'transform', 'matrix(0, 1, 1, 0, 0, 0)'); + cy.get('#img').should('have.css', 'filter', 'brightness(1) grayscale(1) blur(0px) hue-rotate(0deg) opacity(1) contrast(1) saturate(1) sepia(0)'); - /* Download the edited image */ + /* Download the edited image. */ cy.get('#img_save_anchor').click(); cy.get('#img_save_anchor').should('have.text', 'Saving...'); cy.get('#img_save_anchor').should('have.attr', 'aria-disabled', 'true'); cy.wait(imgDownloadTimeoutMS); cy.get('#img_save_anchor').should('have.text', 'Save Image'); - cy.readFile('tests/cypress/downloads/pickle-rick (edited).webp'); + cy.readFile(`${DOWNLOADS_PATH}/pickle-rick (edited).webp`).should('exist'); - /* Upload another image */ - cy.get('#img_drop_zone').selectFile('tests/cypress/fixtures/landscape.jpg', { action: 'drag-drop' }); - cy.get('@grayscaleFilterBtn').should('not.have.class', activeFilterClass); + /* Upload another image. */ + cy.get('#img_drop_zone').selectFile(`${FIXTURES_PATH}/landscape.jpg`, { action: 'drag-drop' }); cy.get('@firstFilterBtn').should('have.class', activeFilterClass); - + cy.get('@grayscaleFilterBtn').should('not.have.class', activeFilterClass); cy.get('#filters_container').should(({ 0: filtersContainer }) => { expect(filtersContainer?.scrollLeft).to.equal(0); }); - /* Manually reset edits */ - cy.get('@rotateLeftBtn').click(); - cy.get('#img').should('have.css', 'transform', 'matrix(0, -1, 1, 0, 0, 0)'); + /* Manually reset applied edits. */ + cy.get('@rotateRightBtn').click(); + cy.get('@grayscaleFilterBtn').click(); + cy.get('#active_filter_range_input').invoke('val', 100); + cy.get('#active_filter_range_input').trigger('input'); cy.get('#reset_filters_btn').click(); - cy.get('#img').should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)'); cy.get('#reset_filters_btn').should('be.disabled'); cy.get('#img_save_anchor').should('have.attr', 'aria-disabled', 'true'); + cy.get('#img').should('have.css', 'transform', INITIAL_CSS_MATRIX); + cy.get('#img').should('have.css', 'filter', INITIAL_CSS_FILTERS); }); diff --git a/tests/cypress/fixtures/sh.png b/tests/cypress/fixtures/sh.png deleted file mode 100644 index 40e35d9..0000000 Binary files a/tests/cypress/fixtures/sh.png and /dev/null differ diff --git a/tests/unit/imgStore/activeFilter.test.ts b/tests/unit/imgStore/activeFilter.test.ts index cc91590..241b905 100644 --- a/tests/unit/imgStore/activeFilter.test.ts +++ b/tests/unit/imgStore/activeFilter.test.ts @@ -1,11 +1,9 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; -describe('imgStore.activeFilter', () => { - it('should update the active filter based on the given name, and allow access to its properties', () => { - const activeFilterName = 'grayscale'; - imgStore.activeFilter = activeFilterName; +test('updates the active filter based on the given name, and allows access to its properties', () => { + const activeFilterName = 'grayscale'; + imgStore.activeFilter = activeFilterName; - expect(imgStore.activeFilter.name).toBe(activeFilterName); - }); + expect(imgStore.activeFilter.name).toBe(activeFilterName); }); diff --git a/tests/unit/imgStore/isEdited.test.ts b/tests/unit/imgStore/isEdited.test.ts index 6805b6d..711c9b7 100644 --- a/tests/unit/imgStore/isEdited.test.ts +++ b/tests/unit/imgStore/isEdited.test.ts @@ -1,14 +1,10 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; -describe('imgStore.isEdited', () => { - it('should return “true” if the image has been rotated', () => { - const rotationDegs = [-450, -270, -180, -90, 90, 180, 270, 450] as const; +const rotationDegs = [-450, -270, -180, -90, 90, 180, 270, 450] as const; - rotationDegs.forEach((rotationDeg) => { - imgStore.state.rotationDeg = rotationDeg; +test.each(rotationDegs)('%i° rotates the image', (rotationDeg) => { + imgStore.state.rotationDeg = rotationDeg; - expect(imgStore.isEdited).toBeTruthy(); - }); - }); + expect(imgStore.isEdited).toBeTruthy(); }); diff --git a/tests/unit/imgStore/isLandscape.test.ts b/tests/unit/imgStore/isLandscape.test.ts index 4e01309..ed838e6 100644 --- a/tests/unit/imgStore/isLandscape.test.ts +++ b/tests/unit/imgStore/isLandscape.test.ts @@ -1,14 +1,10 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; -describe('imgStore.isLandscape', () => { - it('should return “true” if the image has been rotated only by multiples of 90 degrees and not 180 degrees', () => { - const rotationDegs = [-450, -270, -90, 90, 270, 450] as const; +const rotationDegs = [-450, -270, -90, 90, 270, 450] as const; - rotationDegs.forEach((rotationDeg) => { - imgStore.state.rotationDeg = rotationDeg; +test.each(rotationDegs)('%i° is a multiple of 90°, but not that of 180°; so the image is landscaped', (rotationDeg) => { + imgStore.state.rotationDeg = rotationDeg; - expect(imgStore.isLandscape).toBeTruthy(); - }); - }); + expect(imgStore.isLandscape).toBeTruthy(); }); diff --git a/tests/unit/imgStore/reset.test.ts b/tests/unit/imgStore/reset.test.ts index 326f626..5f41cb3 100644 --- a/tests/unit/imgStore/reset.test.ts +++ b/tests/unit/imgStore/reset.test.ts @@ -1,17 +1,15 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; import { deepClone } from '@ts/utils/deepClone.ts'; import { resetRotationDeg } from '@ts/utils/resetRotationDeg.ts'; -describe('imgStore.reset', () => { - it('should correctly reset the state', () => { - const initialState = deepClone(imgStore.state); - const [name, extension, rotationDeg] = ['New Image', 'png', 180]; - const adjustedRotationDeg = resetRotationDeg(rotationDeg); +test('resets the state', () => { + const initialState = deepClone(imgStore.state); + const [name, extension, rotationDeg] = ['New Image', 'png', 180]; + const adjustedRotationDeg = resetRotationDeg(rotationDeg); - Object.assign(imgStore.state, { name, extension, rotationDeg, verticalFlip: -1 }); - imgStore.reset(); + Object.assign(imgStore.state, { name, extension, rotationDeg, verticalFlip: -1 }); + imgStore.reset(); - expect(imgStore.state).toStrictEqual({ ...initialState, name, extension, rotationDeg: adjustedRotationDeg }); - }); + expect(imgStore.state).toStrictEqual({ ...initialState, name, extension, rotationDeg: adjustedRotationDeg }); }); diff --git a/tests/unit/imgStore/title.test.ts b/tests/unit/imgStore/title.test.ts index c158eb4..16deda0 100644 --- a/tests/unit/imgStore/title.test.ts +++ b/tests/unit/imgStore/title.test.ts @@ -1,10 +1,8 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; -describe('imgStore.title', () => { - it('should set the name and extension of the image file, and return its full name (name.extension)', () => { - Object.assign(imgStore.state, { name: 'New Image', extension: 'png' }); +test('sets the name and the extension of the image file, then returns “name.extension”', () => { + Object.assign(imgStore.state, { name: 'New Image', extension: 'png' }); - expect(imgStore.title).toMatchInlineSnapshot(`"New Image.png"`); - }); + expect(imgStore.title).toMatchInlineSnapshot(`"New Image.png"`); }); diff --git a/tests/unit/imgStore/updateCSSFilters.test.ts b/tests/unit/imgStore/updateCSSFilters.test.ts index bb00b6e..ecab25e 100644 --- a/tests/unit/imgStore/updateCSSFilters.test.ts +++ b/tests/unit/imgStore/updateCSSFilters.test.ts @@ -1,12 +1,10 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; -import { it, expect, describe } from 'vitest'; -describe('imgStore.updateCSSFilters', () => { - it('should create and/or update the CSS filters string corresponding with filter values', () => { - imgStore.state.filters.find(({ name }) => name === 'grayscale')!.value = 50; - imgStore.updateCSSFilters(); - const { CSSFilters } = imgStore.state; +test('creates the CSS filters string corresponding with filter values', () => { + imgStore.state.filters.find(({ name }) => name === 'grayscale')!.value = 50; + imgStore.updateCSSFilters(); + const { CSSFilters } = imgStore.state; - expect(CSSFilters).toMatchInlineSnapshot(`"brightness(100%)grayscale(50%)blur(0px)hue-rotate(0deg)opacity(100%)contrast(100%)saturate(100%)sepia(0%)"`); - }); + expect(CSSFilters).toMatchInlineSnapshot(`"brightness(100%)grayscale(50%)blur(0px)hue-rotate(0deg)opacity(100%)contrast(100%)saturate(100%)sepia(0%)"`); }); diff --git a/tests/unit/imgStore/updateSpinValue.test.ts b/tests/unit/imgStore/updateSpinValue.test.ts index c4fa1e5..6d0b199 100644 --- a/tests/unit/imgStore/updateSpinValue.test.ts +++ b/tests/unit/imgStore/updateSpinValue.test.ts @@ -1,21 +1,18 @@ +import { test, expect } from 'vitest'; import { imgStore } from '@ts/imgStore.ts'; import { spinModes } from '@ts/constants.ts'; -import { it, expect, describe } from 'vitest'; import { spinIsRotation } from '@ts/utils/spinIsRotation.ts'; -describe('imgStore.updateSpinValue', () => { - it('should update the rotation degree and vertical/horizontal flip values', () => { - spinModes.forEach((spinMode) => { - imgStore.updateSpinValue(spinMode); +test.each(spinModes)('“%s” updates the rotation degree or vertical/horizontal flip', (spinMode) => { + imgStore.updateSpinValue(spinMode); - const { rotationDeg, verticalFlip, horizontalFlip } = imgStore.state; + const { rotationDeg, verticalFlip, horizontalFlip } = imgStore.state; - if (spinIsRotation(spinMode)) { - expect(rotationDeg === 0 || rotationDeg % 90 === 0).toBeTruthy(); - } else { - expect([1, -1]).toContain(verticalFlip); - expect([1, -1]).toContain(horizontalFlip); - } - }); - }); + // eslint-disable-next-line test/no-conditional-in-test + if (spinIsRotation(spinMode)) { + expect(rotationDeg === 0 || rotationDeg % 90 === 0).toBeTruthy(); + } else { + expect([1, -1]).toContain(verticalFlip); + expect([1, -1]).toContain(horizontalFlip); + } }); diff --git a/tests/unit/utils/deepClone.test.ts b/tests/unit/utils/deepClone.test.ts index 627f396..3272d8d 100644 --- a/tests/unit/utils/deepClone.test.ts +++ b/tests/unit/utils/deepClone.test.ts @@ -1,18 +1,16 @@ -import { it, expect, describe } from 'vitest'; +import { test, expect } from 'vitest'; import { deepClone } from '@ts/utils/deepClone.ts'; -describe('deepClone', () => { - it('should create a deep clone of the given object', () => { - const obj = { - a: 1, - b: true, - c: 'value', - d: { e: 'f' }, - g: [1, '2', { h: '3' }], - }; - const clone = deepClone(obj); +test('creates a deep clone of the given object', () => { + const obj = { + a: 1, + b: true, + c: 'value', + d: { e: 'f' }, + g: [1, '2', { h: '3' }], + }; + const clone = deepClone(obj); - expect(clone).not.toBe(obj); - expect(clone).toStrictEqual(obj); - }); + expect(clone).not.toBe(obj); + expect(clone).toStrictEqual(obj); }); diff --git a/tests/unit/utils/resetRotationDeg.test.ts b/tests/unit/utils/resetRotationDeg.test.ts index 6c2e578..d3c3679 100644 --- a/tests/unit/utils/resetRotationDeg.test.ts +++ b/tests/unit/utils/resetRotationDeg.test.ts @@ -1,15 +1,11 @@ -import { it, expect, describe } from 'vitest'; +import { test, expect } from 'vitest'; import { resetRotationDeg } from '@ts/utils/resetRotationDeg.ts'; +import { rotationDegs as baseRotationDegs } from '@ts/constants.ts'; -describe('resetRotationDeg', () => { - it('should reset the rotation degree to a multiple of 360 degrees', () => { - const fullRotationDeg = 360; - const rotationDegs = [-450, -360, -270, -180, -90, 0, 90, 180, 270, 360, 450]; +const rotationDegs = [-450, -360, -270, -180, -90, 0, 90, 180, 270, 360, 450]; - rotationDegs.forEach((rotationDeg) => { - const adjustedRotationDeg = resetRotationDeg(rotationDeg); +test.each(rotationDegs)('%i° is reset to a multiple of 360°', (rotationDeg) => { + const adjustedRotationDeg = resetRotationDeg(rotationDeg); - expect(adjustedRotationDeg % fullRotationDeg).toBe(0); - }); - }); + expect(adjustedRotationDeg % baseRotationDegs.full).toBe(0); }); diff --git a/tests/unit/utils/spinIsRotation.test.ts b/tests/unit/utils/spinIsRotation.test.ts index deed343..3348823 100644 --- a/tests/unit/utils/spinIsRotation.test.ts +++ b/tests/unit/utils/spinIsRotation.test.ts @@ -1,12 +1,8 @@ -import { it, expect, describe } from 'vitest'; +import { test, expect } from 'vitest'; import { spinIsRotation } from '@ts/utils/spinIsRotation.ts'; -describe('spinIsRotation', () => { - it('should return “true” if the given spin mode is a rotation', () => { - const rotationModes = ['Rotate Right', 'Rotate Left'] as const; +const rotationModes = ['Rotate Right', 'Rotate Left'] as const; - rotationModes.forEach((rotationMode) => { - expect(spinIsRotation(rotationMode)).toBeTruthy(); - }); - }); +test.each(rotationModes)('“%s” is a rotation', (rotationMode) => { + expect(spinIsRotation(rotationMode)).toBeTruthy(); }); diff --git a/vitest.config.ts b/vitest.config.ts index c5d37c0..af1319b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,13 +2,9 @@ import { getViteConfig } from 'astro/config'; export default getViteConfig({ test: { + include: ['tests/unit/**/*.test.ts'], expect: { requireAssertions: true, }, - typecheck: { - enabled: true, - allowJs: true, - tsconfig: './tsconfig.json', - }, }, });