diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c0a6054..35f3ea9 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -17,3 +17,34 @@ jobs: - run: npm run lint - run: npm run ts:check - run: npm run build + + - name: Cache build folder + id: cache-build + uses: actions/cache/save@v3 + with: + path: ${{ github.workspace }}/build + key: ${{ github.sha }}-build + tests: + runs-on: ubuntu-latest + needs: + - build + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/cache/restore@v3 + id: restore-build + with: + path: ${{ github.workspace }}/build + key: ${{ github.sha }}-build + - uses: actions/setup-node@v3 + with: + node-version: 20 + - run: npm i + - run: npx playwright install --with-deps + - run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 078e1a7..18bdf1f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,10 @@ android/app/build android/.gradle android/app-release-* android/*.keystore -android/.idea \ No newline at end of file +android/.idea + +# Playwright +/test-results/ +/playwright-report/ +/playwright/.cache/ +/screenshots \ No newline at end of file diff --git a/e2e/colorScheme.spec.ts b/e2e/colorScheme.spec.ts new file mode 100644 index 0000000..987489c --- /dev/null +++ b/e2e/colorScheme.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from "@playwright/test"; + +test("colorscheme interface", async ({ page }) => { + await page.goto("localhost:3000"); + + await expect( + page.locator("html[data-mantine-color-scheme=light]"), + ).toBeVisible(); + + await page.getByRole("button", { name: "Toggle color scheme" }).click(); + + await expect( + page.locator("html[data-mantine-color-scheme=light]"), + ).not.toBeVisible(); + await expect( + page.locator("html[data-mantine-color-scheme=dark]"), + ).toBeVisible(); +}); diff --git a/e2e/colorScheme.spec.ts-snapshots/colorscheme-interface-1-chromium-darwin.png b/e2e/colorScheme.spec.ts-snapshots/colorscheme-interface-1-chromium-darwin.png new file mode 100644 index 0000000..d8a548a Binary files /dev/null and b/e2e/colorScheme.spec.ts-snapshots/colorscheme-interface-1-chromium-darwin.png differ diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts new file mode 100644 index 0000000..815e71d --- /dev/null +++ b/e2e/dashboard.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "@playwright/test"; + +test("default blocks are visible", async ({ page }) => { + await page.goto("localhost:3000"); + + await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible(); + await expect(page.getByTestId("recently-play-empty")).toBeVisible(); + await expect(page.getByTestId("recent-favorites")).toBeVisible(); + await expect( + page.getByRole("list", { name: "Moods & genres" }).getByRole("listitem"), + ).toHaveCount(113); +}); diff --git a/e2e/dashboard.spec.ts-snapshots/default-blocks-are-visible-1-chromium-darwin.png b/e2e/dashboard.spec.ts-snapshots/default-blocks-are-visible-1-chromium-darwin.png new file mode 100644 index 0000000..98264b3 Binary files /dev/null and b/e2e/dashboard.spec.ts-snapshots/default-blocks-are-visible-1-chromium-darwin.png differ diff --git a/e2e/favorites.spec.ts b/e2e/favorites.spec.ts new file mode 100644 index 0000000..2ec2f1a --- /dev/null +++ b/e2e/favorites.spec.ts @@ -0,0 +1,60 @@ +import { expect, test } from "@playwright/test"; + +import { navigateTo, selectedInstance } from "./utils"; + +test("save card in favorites", async ({ page }) => { + await page.goto("localhost:3000"); + await selectedInstance(page, "invidious.fdn.fr"); + + // Go to Trending page + await page.getByRole("button", { name: "Trending" }).click(); + await expect(page.getByRole("heading", { name: "Trending" })).toBeVisible(); + await expect(page.getByRole("list", { name: "Trending" })).toBeVisible(); + + // Toggle favorite button + await page.getByRole("button", { name: "Add to favorite" }).first().click(); + await expect( + page.getByRole("button", { name: "Remove from favorite" }), + ).toBeVisible(); + + // Verify notification visibility + await expect(page.getByRole("alert")).toContainText(/Added to favorites/); + + // Go to Favorites page + await navigateTo(page, "Favorites", "Favorites"); + + // Check tab navigation + await expect(page.getByRole("tablist").getByRole("tab")).toHaveCount(5); + // Check default tab is All + await expect( + page.getByRole("tablist").getByRole("tab", { + selected: true, + }), + ).toContainText(/All/); + + // Check data-list + await expect( + page.getByRole("list", { name: "Favorites list" }).getByRole("listitem"), + ).toHaveCount(1); + await expect( + page.getByRole("button", { name: "Remove from favorite" }), + ).toBeVisible(); + + // Switch tab to Videos + await page.getByRole("tablist").getByRole("tab", { name: "Videos" }).click(); + await expect( + page.getByRole("tablist").getByRole("tab", { + selected: true, + }), + ).toContainText(/Videos/); + + // Check data-list displayed + await expect( + page + .getByRole("list", { name: "Favorites videos list" }) + .getByRole("listitem"), + ).toHaveCount(1); + await expect( + page.getByRole("button", { name: "Remove from favorite" }), + ).toBeVisible(); +}); diff --git a/e2e/player.spec.ts b/e2e/player.spec.ts new file mode 100644 index 0000000..efc99d9 --- /dev/null +++ b/e2e/player.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "@playwright/test"; + +import { search, selectedInstance } from "./utils"; + +test.describe.serial("player", () => { + test.skip("play video", async ({ page }) => { + await page.goto("localhost:3000"); + + await selectedInstance(page, "invidious.fdn.fr"); + + await page.reload(); + + await search(page, "Eminem lyric"); + + await page + .getByRole("list", { name: "Search result list Eminem lyric" }) + .getByRole("listitem") + .first() + .click(); + + await expect(page.getByRole("dialog", { name: "Player" })).toBeVisible({ + timeout: 15000, + }); + + // Ne fonctionne pas + + await expect( + page.getByRole("status", { name: "Player loading" }), + ).toBeVisible({ + timeout: 15000, + }); + }); +}); diff --git a/e2e/playlists.spec.ts b/e2e/playlists.spec.ts new file mode 100644 index 0000000..124fb25 --- /dev/null +++ b/e2e/playlists.spec.ts @@ -0,0 +1,217 @@ +import { expect, test } from "@playwright/test"; + +import { + checkNotification, + navigateTo, + search, + selectSearchType, + selectedInstance, +} from "./utils"; + +const createPlaylist = async (page, title: string) => { + await selectedInstance(page, "invidious.fdn.fr"); + await navigateTo(page, "Playlists", "Playlists"); + + // Open modal to create playlist + await page + .getByRole("button", { name: "Open modal to create playlist" }) + .click(); + + // Fill form to create playlist + await page + .getByRole("form", { name: "Form create playlist" }) + .getByPlaceholder("My awesome title") + .fill(title); + await page + .getByRole("form", { name: "Form create playlist" }) + .getByRole("button", { name: "Create playlist" }) + .click(); + + // Verify notification visibility + await checkNotification(page, /has been created/); +}; + +test.describe.serial("playlists", () => { + test("create playlist and add video", async ({ page }) => { + await page.goto("localhost:3000"); + await createPlaylist(page, "My first playlist"); + + // Check some elements on the playlist card + await expect( + page.getByRole("heading", { name: "My first playlist" }), + ).toBeVisible(); + await expect(page.getByRole("listitem")).toContainText("0 videos"); + + // Go to playlist detail + await page.getByRole("heading", { name: "My first playlist" }).click(); + await expect( + page.getByRole("heading", { name: "My first playlist" }), + ).toBeVisible(); + await expect(page.getByRole("alert")).toContainText( + /My first playlist is empty/, + ); + + // Go back to playlists + await page.getByRole("button", { name: "Back to previous page" }).click(); + await expect( + page.getByRole("heading", { name: "Playlists" }), + ).toBeVisible(); + + // Go to trending and wait for the list to be visible + await navigateTo(page, "Trending", "Trending"); + await expect(page.getByRole("list", { name: "Trending" })).toBeVisible({ + timeout: 10000, + }); + + await page + .getByRole("listitem") + .first() + .getByRole("button", { name: "Card menu" }) + .click(); + + await expect(page.getByRole("menu")).toBeVisible(); + await page + .getByRole("menu") + .getByRole("menuitem", { name: "Add to playlist" }) + .click(); + + await expect( + page.getByRole("form", { name: "Form add to playlist" }), + ).toBeVisible(); + await expect( + page + .getByRole("form", { name: "Form add to playlist" }) + .getByRole("button", { name: "Add to playlist" }), + ).not.toBeEnabled(); + await page + .getByRole("form", { name: "Form add to playlist" }) + .getByPlaceholder("Your playlist") + .click(); + await page.locator('[value="My first playlist"]').click(); + await expect( + page + .getByRole("form", { name: "Form add to playlist" }) + .getByRole("button", { name: "Add to playlist" }), + ).toBeEnabled(); + await page + .getByRole("form", { name: "Form add to playlist" }) + .getByRole("button", { name: "Add to playlist" }) + .click(); + + // Verify notification visibility + await checkNotification(page, /Added to playlist/); + + // Go to playlists + await navigateTo(page, "Playlists", "Playlists"); + + await expect(page.getByRole("listitem")).toContainText("1 videos"); + + // Save remote playlist to user playlists + await selectSearchType(page, "playlist"); + await search(page, "Tomorrowland 2018"); + + await page + .getByRole("list", { name: "Search result list Tomorrowland 2018" }) + .getByRole("listitem") + .first() + .getByRole("button", { + name: "Save to playlists", + }) + .click(); + + await checkNotification(page, /added to your playlists list/); + + await page + .getByRole("list", { name: "Search result list Tomorrowland 2018" }) + .getByRole("listitem") + .nth(3) + .getByRole("button", { + name: "Save to playlists", + }) + .click(); + + await checkNotification(page, /added to your playlists list/); + + await navigateTo(page, "Playlists", "Playlists"); + + await page.reload(); + + // Check persisted data + await expect( + page.getByRole("heading", { name: "Playlists" }), + ).toBeVisible(); + await expect( + page.getByRole("list", { name: "Playlists list" }).getByRole("listitem"), + ).toHaveCount(3); + }); + + test("edit playlist", async ({ page }) => { + await page.goto("localhost:3000"); + await createPlaylist(page, "Awesome playlist"); + + // Open playlist menu + await page.getByRole("button", { name: "Open playlist menu" }).click(); + await page + .getByRole("menu") + .getByRole("menuitem", { name: "Edit" }) + .click(); + + await expect( + page.getByRole("form", { name: "Form update playlist" }), + ).toBeVisible(); + await page + .getByRole("form", { name: "Form update playlist" }) + .getByPlaceholder("My awesome title") + .fill("Awesome playlist updated"); + await page + .getByRole("form", { name: "Form update playlist" }) + .getByRole("button", { name: "Update playlist" }) + .click(); + + // Verify notification visibility + await checkNotification(page, /Awesome playlist updated has been updated/); + + await page.reload(); + + // Check persisted data + await expect( + page.getByRole("heading", { name: "Playlists" }), + ).toBeVisible(); + await expect( + page.getByRole("list", { name: "Playlists list" }).getByRole("listitem"), + ).toHaveCount(1); + await expect( + page + .getByRole("listitem") + .getByRole("heading", { name: "Awesome playlist updated" }), + ).toBeVisible(); + }); + + test("remove playlist", async ({ page }) => { + await page.goto("localhost:3000"); + await createPlaylist(page, "Awesome playlist"); + + // Open playlist menu + await page.getByRole("button", { name: "Open playlist menu" }).click(); + await page + .getByRole("menu") + .getByRole("menuitem", { name: "Delete" }) + .click(); + + await expect( + page.getByRole("form", { name: "Delete playlist" }), + ).toBeVisible(); + await expect( + page.getByRole("form", { name: "Delete playlist" }), + ).toContainText(/Do you want deleted Awesome playlist playlist ?/); + await page + .getByRole("form", { name: "Delete playlist" }) + .getByRole("button", { name: "Delete playlist" }) + .click(); + + await expect(page.getByTestId("playlists-empty")).toBeVisible(); + await expect(page.getByTestId("playlists-empty")).toContainText( + /You have no playlist/, + ); + }); +}); diff --git a/e2e/search.spec.ts b/e2e/search.spec.ts new file mode 100644 index 0000000..6465289 --- /dev/null +++ b/e2e/search.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from "@playwright/test"; + +import { listVisibility, search, selectSearchType, selectedInstance } from "./utils"; + +test.describe.serial("search", () => { + test("search an artist", async ({ page }) => { + await page.goto("localhost:3000"); + + await selectedInstance(page, "invidious.fdn.fr"); + + await search(page, "Eminem"); + await search(page, "Dubstep"); + + // Focus input text + await page.getByPlaceholder("What do you want hear today?").fill(""); + + // Check the search history submenu visibility on focused input + await expect(page.getByTestId("Search history submenu")).toBeVisible(); // Why not work with getByRole('dialog', ...) ? + + // Verify the search history submenu content + await expect( + page + .getByTestId("Search history submenu") + .getByRole("button", { name: "Eminem" }), + ).toBeVisible(); + await expect( + page + .getByTestId("Search history submenu") + .getByRole("button", { name: "Dubstep" }), + ).toBeVisible(); + + // Select previous search + await page + .getByTestId("Search history submenu") + .getByRole("button", { name: "Eminem" }) + .click(); + await expect( + page.getByPlaceholder("What do you want hear today?"), + ).toHaveValue("Eminem"); + // Check search results with selected search history + await listVisibility(page, "Eminem"); + }); + + test("search filters", async ({ page }) => { + await page.goto("localhost:3000"); + + await search(page, "Defqon 2018"); + await selectSearchType(page, "playlist"); + await listVisibility(page, "Defqon 2018"); + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts new file mode 100644 index 0000000..772245f --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,85 @@ +import { Page, expect } from "@playwright/test"; + +export const navigateTo = async ( + page: Page, + label: string, + heading: string, +) => { + await page + .getByRole("navigation", { name: "App navigation" }) + .getByRole("button", { name: label }) + .click(); + await expect( + page + .getByRole("navigation", { name: "App navigation" }) + .getByRole("button", { name: label }), + ).toHaveAttribute("aria-selected", "true"); + await expect(page.getByRole("heading", { name: heading })).toBeVisible(); +}; + +export const checkNotification = async (page: Page, value: RegExp) => { + await expect(page.getByRole("alert")).toContainText(value); + await page.getByRole("alert").getByRole("button").click(); + await expect(page.getByRole("alert")).not.toBeVisible(); +}; + +export const search = async (page: Page, value: string) => { + // Check search bar visibility + await expect( + page.getByRole("form", { name: "Search bar form" }), + ).toBeVisible(); + await page.getByPlaceholder("What do you want hear today?").fill(value); + await page.getByPlaceholder("What do you want hear today?").press("Enter"); + await listVisibility(page, value); +}; + +export const selectSearchType = async ( + page: Page, + type: string, + currentType: string = "Videos", +) => { + await page.getByRole("button", { name: "Open search filters" }).click(); + await expect( + page.getByRole("menu", { name: "Search filters" }), + ).toBeVisible(); + await page + .getByRole("menu", { name: "Search filters" }) + .getByRole("textbox", { name: "Type filter" }) + .click(); + // await expect( + // page.getByRole("listbox").getByRole("option", { selected: true }), + // ).toContainText(currentType); + // await page.getByRole("listbox").getByRole("option", { name: type }).click(); + await expect(page.locator('[role="option"][value="video"][aria-selected="true"]')).toContainText(currentType); + await page.locator(`[role="option"][value=${type}]`).click(); +}; + +export const listVisibility = async (page: Page, label: string) => { + await expect( + page.getByRole("heading", { name: `Search results : ${label}` }), + ).toBeVisible(); + await expect( + page.getByRole("list", { name: `Search result list ${label}` }), + ).toBeVisible(); + await expect( + page.getByRole("list", { name: `Search result list ${label}` }), + ).not.toBeEmpty(); +}; + +export const selectedInstance = async (page: Page, instanceUri: string) => { + await navigateTo(page, "Settings", "Settings"); + await page.getByRole("button", { name: /General/ }).click(); + await expect( + page.getByRole("list", { name: "Invidious instances list" }), + ).toBeVisible(); + await expect( + page.getByRole("listitem", { name: "invidious.fdn.fr" }), + ).toBeVisible(); + await page + .getByRole("listitem", { name: "invidious.fdn.fr" }) + .getByTestId("use") + .click(); + await expect( + page.getByRole("listitem", { name: "invidious.fdn.fr" }), + ).toHaveAttribute("aria-current", "true"); +}; diff --git a/package-lock.json b/package-lock.json index 0c8a068..a36630e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "@commitlint/config-conventional": "^18.4.3", "@craco/craco": "^7.1.0", "@craco/types": "^7.1.0", + "@playwright/test": "^1.39.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "eslint": "^8.54.0", "eslint-plugin-prettier": "^5.0.1", @@ -5867,6 +5868,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "dependencies": { + "playwright": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -16290,6 +16306,36 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "dependencies": { + "playwright-core": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 0841aa4..1543b5a 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "license": "MIT", "private": true, "scripts": { - "postinstall": "cp .env.dist .env", + "postinstall": "cp .env.dist .env && playwright install", "start": "craco start", "build": "craco build", "ts:check": "tsc", "lint": "eslint src", - "format": "prettier --write ./src" + "format": "prettier --write ./src", + "e2e:start": "playwright test --config ./playwright.config.ts --ui" }, "dependencies": { "@codemotion/color-thief": "^2.0.5", @@ -90,6 +91,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "eslint": "^8.54.0", "eslint-plugin-prettier": "^5.0.1", + "@playwright/test": "^1.39.0", "husky": "^8.0.3", "prettier": "^3.1.0", "typescript": "^5.3.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..1ac3a02 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* 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://127.0.0.1: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: "npx serve ./build", + url: "http://127.0.0.1:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/ButtonFavorite.tsx b/src/components/ButtonFavorite.tsx index dafad54..56cb7e7 100644 --- a/src/components/ButtonFavorite.tsx +++ b/src/components/ButtonFavorite.tsx @@ -167,6 +167,9 @@ export const ButtonFavorite: FC = memo( radius="md" size={buttonSize} onClick={onClick} + aria-label={t( + isFavorite ? "button.favorite.remove" : "button.favorite.add", + )} > diff --git a/src/components/ButtonHistoryBack.tsx b/src/components/ButtonHistoryBack.tsx index 7f99b1e..1a393e9 100644 --- a/src/components/ButtonHistoryBack.tsx +++ b/src/components/ButtonHistoryBack.tsx @@ -15,7 +15,13 @@ export const ButtonHistoryBack = memo(() => { return ( - + diff --git a/src/components/CardList.tsx b/src/components/CardList.tsx index f23871b..a22b07f 100644 --- a/src/components/CardList.tsx +++ b/src/components/CardList.tsx @@ -10,12 +10,13 @@ import { PlaylistCard } from "./PlaylistCard"; import { VideoCard } from "./VideoCard"; interface CardListProps { + label?: string; data: CardType[]; scrollable?: boolean; } export const CardList: FC = memo( - ({ data, scrollable = false }) => { + ({ label, data, scrollable = false }) => { const { currentInstance } = useSettings(); if (!data.length) { @@ -23,10 +24,15 @@ export const CardList: FC = memo( } return ( - + {data.map((card, index) => ( {(() => { diff --git a/src/components/CardMenu.tsx b/src/components/CardMenu.tsx index 7ff1d49..766c582 100644 --- a/src/components/CardMenu.tsx +++ b/src/components/CardMenu.tsx @@ -28,7 +28,7 @@ export const CardMenu: FC = memo(({ card }) => { return ( <> - + @@ -43,6 +43,7 @@ export const CardMenu: FC = memo(({ card }) => { onClick={() => setDeleteFromPlaylistModalOpened(true)} color="red" leftSection={} + aria-label={t("menu.video.remove.playlist")} > {t("menu.video.remove.playlist")} @@ -50,6 +51,7 @@ export const CardMenu: FC = memo(({ card }) => { setAddToPlaylistModalOpened(true)} leftSection={} + aria-label={t("menu.video.add.playlist")} > {t("menu.video.add.playlist")} diff --git a/src/components/ColorScheme.tsx b/src/components/ColorScheme.tsx index 39c34ee..099daab 100644 --- a/src/components/ColorScheme.tsx +++ b/src/components/ColorScheme.tsx @@ -23,6 +23,7 @@ export const ColorScheme = memo(() => { style={{ height: 36, width: 36 }} onClick={() => toggleColorScheme()} title="Toggle color scheme" + aria-label="Toggle color scheme" > {colorScheme === "dark" ? ( diff --git a/src/components/FavoritePlaylist.tsx b/src/components/FavoritePlaylist.tsx index adee808..278dcfa 100644 --- a/src/components/FavoritePlaylist.tsx +++ b/src/components/FavoritePlaylist.tsx @@ -61,19 +61,35 @@ export const FavoritePlaylist = memo(() => { - + - {!videos.length ? : } + {!videos.length ? ( + + ) : ( + + )} - {!livestream.length ? : } + {!livestream.length ? ( + + ) : ( + + )} - {!playlists.length ? : } + {!playlists.length ? ( + + ) : ( + + )} - {!channels.length ? : } + {!channels.length ? ( + + ) : ( + + )} ); @@ -81,14 +97,15 @@ export const FavoritePlaylist = memo(() => { interface DataListProps { data: Card[]; + label?: string; } -const DataList: FC = memo(({ data: initialData }) => { +const DataList: FC = memo(({ label, data: initialData }) => { const { data, ref } = usePaginateData(initialData); return ( <> - +