No files matched your search
@@ -1,8 +1,9 @@
|
||||
REACT_APP_TIDAL_SEARCH_TOKEN=CzET4vdadNUFQ5JU
|
||||
REACT_APP_TIDAL_COUNTRY_CODE=CA
|
||||
REACT_APP_TIDARR_SEARCH_URL=http://www.tidal.com/album/
|
||||
REACT_APP_TIDARR_VERSION=0.0.7
|
||||
|
||||
TIDARR_REPO_URL=cstaelen/tidarr
|
||||
TIDARR_VERSION=0.0.8
|
||||
ENABLE_BEETS=
|
||||
ENABLE_PLEX_UPDATE=
|
||||
PLEX_URL=
|
||||
|
||||
@@ -24,6 +24,16 @@ jobs:
|
||||
with:
|
||||
images: cstaelen/tidarr
|
||||
|
||||
- name: Build and push pre-release docker image
|
||||
if: 'github.event.prerelease'
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.prod
|
||||
push: true
|
||||
tags: cstaelen/tidarr:prerelease
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Build and push release docker image
|
||||
if: '!github.event.prerelease'
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
@@ -32,14 +42,4 @@ jobs:
|
||||
file: ./docker/Dockerfile.prod
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Build and push pre-release docker image
|
||||
if: 'github.event.prerelease'
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.prod
|
||||
push: true
|
||||
tags: cstaelen/tidarr:prelease
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -53,6 +53,14 @@ jobs:
|
||||
--output-file eslint-results.sarif
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check code lint and format for E2E
|
||||
run: npx eslint ./app
|
||||
--config ./e2e/.eslintrc.json
|
||||
--ext .js,.jsx,.ts,.tsx
|
||||
--format @microsoft/eslint-formatter-sarif
|
||||
--output-file eslint-results.sarif
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload analysis results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Run test suite
|
||||
run: |
|
||||
docker compose logs testing
|
||||
docker compose exec -w /home/app/build/e2e testing npx playwright test
|
||||
docker compose exec -e CI=true -w /home/app/build/e2e testing npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
||||
@@ -3,6 +3,12 @@ Tidarr notable changes.
|
||||
|
||||
[Keep a Changelog](http://keepachangelog.com/en/1.0.0/) format.
|
||||
|
||||
## 📦 0.0.8
|
||||
### 🚀 Added
|
||||
* [Front] Add update checker
|
||||
* [Front] Add configuration dialog
|
||||
* [CI] Add and fix flaky playwright tests
|
||||
|
||||
## 📦 0.0.7
|
||||
### 🚀 Added
|
||||
* [Chore] Add Github CI
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
IMAGE=cstaelen/tidarr
|
||||
VERSION=0.0.7
|
||||
VERSION=0.0.8
|
||||
DOCKERFILE=./docker/Dockerfile.prod
|
||||
DOCKER_COMPOSE = $(or docker compose, docker-compose)
|
||||
|
||||
@@ -20,11 +20,11 @@ testing-build: ## Build container with Playwright tests and production build ima
|
||||
|
||||
testing-run: ## Run Playwright tests with production build image (arg: f=filter)
|
||||
$(DOCKER_COMPOSE) restart testing
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e testing npx playwright test $(f)
|
||||
$(DOCKER_COMPOSE) exec -e IS_DOCKER=true -w /home/app/build/e2e testing npx playwright test $(f)
|
||||
|
||||
testing-update-snapshots: ## Update Playwright snapshots
|
||||
testing-update-snapshots: ## Update Playwright snapshots (arg: f=filter)
|
||||
$(DOCKER_COMPOSE) restart testing
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e testing npx playwright test --reporter=list --update-snapshots
|
||||
$(DOCKER_COMPOSE) exec -e IS_DOCKER=true -w /home/app/build/e2e testing npx playwright test $(f) --reporter=list --update-snapshots
|
||||
|
||||
testing-show-report: ## Show last playwright report
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e testing npx playwright show-report --host 0.0.0.0
|
||||
@@ -39,18 +39,22 @@ testing-clean: ## Clean Playwright reports
|
||||
quality-deadcode: ## Fin deadcode with `ts-prune`
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/api tidarr yarn find-deadcode
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/app tidarr yarn find-deadcode
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e tidarr yarn find-deadcode
|
||||
|
||||
quality-depcheck: ## Check dependencies
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/api tidarr yarn depcheck
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/app tidarr yarn depcheck
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e tidarr yarn depcheck
|
||||
|
||||
quality-lint: ## Check dependencies
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/api tidarr yarn eslint
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/app tidarr yarn eslint
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e tidarr yarn eslint
|
||||
|
||||
quality-lint-fix: ## Check dependencies
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/api tidarr yarn eslint-fix
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/app tidarr yarn eslint-fix
|
||||
$(DOCKER_COMPOSE) exec -w /home/app/build/e2e tidarr yarn eslint-fix
|
||||
|
||||
##
|
||||
## Builder 🚀
|
||||
|
||||
@@ -13,5 +13,25 @@ export async function configureServer() {
|
||||
return {
|
||||
noToken: output_config?.includes("No token found"),
|
||||
output: output_config,
|
||||
parameters: {
|
||||
api: {
|
||||
ENABLE_BEETS: process.env.ENABLE_BEETS,
|
||||
ENABLE_PLEX_UPDATE: process.env.ENABLE_PLEX_UPDATE,
|
||||
PLEX_URL: process.env.PLEX_URL,
|
||||
PLEX_LIBRARY: process.env.PLEX_LIBRARY,
|
||||
PLEX_TOKEN: process.env.PLEX_TOKEN,
|
||||
PLEX_PATH: process.env.PLEX_PATH,
|
||||
ENABLE_GOTIFY: process.env.ENABLE_GOTIFY,
|
||||
GOTIFY_URL: process.env.GOTIFY_URL,
|
||||
GOTIFY_TOKEN: process.env.GOTIFY_TOKEN,
|
||||
TIDARR_VERSION: process.env.TIDARR_VERSION,
|
||||
TIDARR_REPO_URL: process.env.TIDARR_REPO_URL,
|
||||
},
|
||||
app: {
|
||||
REACT_APP_TIDAL_SEARCH_TOKEN: process.env.REACT_APP_TIDAL_SEARCH_TOKEN,
|
||||
REACT_APP_TIDAL_COUNTRY_CODE: process.env.REACT_APP_TIDAL_COUNTRY_CODE,
|
||||
REACT_APP_TIDARR_SEARCH_URL: process.env.REACT_APP_TIDARR_SEARCH_URL,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/favicon/site.webmanifest">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/favicon/manifest.json">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ThemeProvider } from "@emotion/react";
|
||||
import { CssBaseline, createTheme } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DialogNoAPI } from "./components/Dialog/DialogNoAPI";
|
||||
import styled from "@emotion/styled";
|
||||
import { DialogToken } from "./components/Dialog/DialogToken";
|
||||
import { ProcessingList } from "./components/Processing/ProcessingList";
|
||||
import { Results } from "./components/Results";
|
||||
@@ -10,16 +11,19 @@ import {
|
||||
useProcessingProvider,
|
||||
} from "./provider/ProcessingProvider";
|
||||
import { SearchProvider } from "./provider/SearchProvider";
|
||||
import styled from "@emotion/styled";
|
||||
import { GitHub } from "@mui/icons-material";
|
||||
import { DialogConfig } from "./components/Dialog/DialogConfig";
|
||||
import { ConfigProvider } from "./provider/ConfigProvider";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
declare module "@mui/material/styles/createPalette" {
|
||||
interface Palette {
|
||||
gold: string;
|
||||
alert: string;
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
gold: string;
|
||||
alert: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +31,7 @@ const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
gold: "#a57c00",
|
||||
alert: "#e47964",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,55 +47,30 @@ function App() {
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<main className="flex min-h-screen flex-col items-center justify-between">
|
||||
<div className="relative">
|
||||
<SearchProvider>
|
||||
<ProcessingProvider>
|
||||
<Content>
|
||||
<Results />
|
||||
</Content>
|
||||
<ProcessingList />
|
||||
<DialogToken />
|
||||
<DialogNoAPI />
|
||||
</ProcessingProvider>
|
||||
</SearchProvider>
|
||||
</div>
|
||||
</main>
|
||||
<Support>
|
||||
👋{" "}
|
||||
<strong>
|
||||
Private use only. Do not forget to support your local artists 🙏❤️
|
||||
</strong>{" "}
|
||||
• Tidarr • <span>v{window._env_.REACT_APP_TIDARR_VERSION}</span> •{" "}
|
||||
<a href="https://github.com/cstaelen/tidarr" target="_blank">
|
||||
<GitHub />
|
||||
</a>
|
||||
</Support>
|
||||
<ConfigProvider>
|
||||
<main className="flex min-h-screen flex-col items-center justify-between">
|
||||
<div className="relative">
|
||||
<SearchProvider>
|
||||
<ProcessingProvider>
|
||||
<Content>
|
||||
<Results />
|
||||
</Content>
|
||||
<ProcessingList />
|
||||
<DialogToken />
|
||||
<DialogNoAPI />
|
||||
<DialogConfig />
|
||||
</ProcessingProvider>
|
||||
</SearchProvider>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</ConfigProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
const Support = styled.div`
|
||||
background-color: rgb(144, 202, 249);
|
||||
color: #393939;
|
||||
font-weight: normal;
|
||||
padding: 0.3rem;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
|
||||
a {
|
||||
color: black;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin: 0 0 3rem 0;
|
||||
`;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Paper,
|
||||
Tab,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tabs,
|
||||
} from "@mui/material";
|
||||
import { DialogHandler } from ".";
|
||||
import { InfoRounded } from "@mui/icons-material";
|
||||
import { useConfigProvider } from "src/provider/ConfigProvider";
|
||||
|
||||
const TableParameters = ({ rows }: { rows: [string, string][] }) => {
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<strong>Environment vars</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Value</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow
|
||||
key={row[0]}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell>{row[0]}</TableCell>
|
||||
<TableCell>{row[1]}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const DialogConfig = () => {
|
||||
const { config, isUpdateAvailable, releaseData, isConfigModalOpen, actions } =
|
||||
useConfigProvider();
|
||||
|
||||
const [currentTab, setCurrentTab] = React.useState(0);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setCurrentTab(newValue);
|
||||
};
|
||||
|
||||
if (!isConfigModalOpen) return null;
|
||||
|
||||
return (
|
||||
<DialogHandler
|
||||
onClose={() => actions.toggleModal(false)}
|
||||
title={
|
||||
<>
|
||||
<InfoRounded />
|
||||
|
||||
{"Settings"}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={handleTabChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="scrollable auto tabs example"
|
||||
>
|
||||
<Tab label="Updates" />
|
||||
<Tab label="API" />
|
||||
<Tab label="Application" />
|
||||
</Tabs>
|
||||
|
||||
{currentTab === 0 && (
|
||||
<>
|
||||
<p>Current version: {config?.api.TIDARR_VERSION}</p>
|
||||
{isUpdateAvailable ? (
|
||||
<>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<strong>Update available: {releaseData?.name}</strong>
|
||||
</Paper>
|
||||
<p>To update image stop container and run :</p>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<code>docker compose pull tidarr</code>
|
||||
</Paper>
|
||||
</>
|
||||
) : (
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<strong>
|
||||
{isUpdateAvailable
|
||||
? `Update available: ${releaseData?.name}`
|
||||
: "Tidarr is up to date."}
|
||||
</strong>
|
||||
</Paper>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentTab === 1 && (
|
||||
<>
|
||||
{config?.api ? (
|
||||
<TableParameters rows={Object.entries(config.api)} />
|
||||
) : (
|
||||
"Not found."
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentTab === 2 && (
|
||||
<>
|
||||
{config?.app ? (
|
||||
<TableParameters rows={Object.entries(config.app)} />
|
||||
) : (
|
||||
"Not found."
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogHandler>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import WarningIcon from "@mui/icons-material/Warning";
|
||||
import { DialogHandler } from ".";
|
||||
import { useProcessingProvider } from "src/provider/ProcessingProvider";
|
||||
import { useConfigProvider } from "src/provider/ConfigProvider";
|
||||
|
||||
export const DialogToken = () => {
|
||||
const { tokenMissing } = useProcessingProvider();
|
||||
const { tokenMissing } = useConfigProvider();
|
||||
|
||||
if (!tokenMissing) return null;
|
||||
|
||||
|
||||
@@ -11,14 +11,17 @@ import { ReactNode, useState } from "react";
|
||||
export const DialogHandler = ({
|
||||
children,
|
||||
title,
|
||||
onClose,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: ReactNode;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { GitHub, Warning } from "@mui/icons-material";
|
||||
import { Button, Link, useTheme } from "@mui/material";
|
||||
import { useConfigProvider } from "src/provider/ConfigProvider";
|
||||
|
||||
export const Footer = () => {
|
||||
const { isUpdateAvailable, actions, config } = useConfigProvider();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Support className="footer">
|
||||
👋{" "}
|
||||
<strong>
|
||||
Private use only. Do not forget to support your local artists 🙏❤️
|
||||
</strong>{" "}
|
||||
• Tidarr • <span>v{config?.api.TIDARR_VERSION}</span> •
|
||||
{isUpdateAvailable ? (
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ color: theme.palette.alert }}
|
||||
onClick={() => actions.toggleModal(true)}
|
||||
color="warning"
|
||||
startIcon={<Warning />}
|
||||
>
|
||||
<strong>Update available</strong>
|
||||
</Button>
|
||||
) : (
|
||||
<Link onClick={() => actions.toggleModal(true)} component="button">
|
||||
<strong>Configuration</strong>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={`https://github.com/${config?.api.TIDARR_REPO_URL}`}
|
||||
target="_blank"
|
||||
sx={{
|
||||
verticalAlign: "middle",
|
||||
color: theme.palette.common.white,
|
||||
paddingLeft: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<GitHub />
|
||||
</Link>
|
||||
</Support>
|
||||
);
|
||||
};
|
||||
|
||||
const Support = styled.div`
|
||||
background-color: rgb(54, 54, 54);
|
||||
font-size: 0.825rem;
|
||||
font-weight: normal;
|
||||
padding: 0.3rem;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
`;
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, { useContext, useState, ReactNode, useEffect } from "react";
|
||||
import {
|
||||
ApiReturnType,
|
||||
ConfigParametersType,
|
||||
ConfigType,
|
||||
ReleaseGithubType,
|
||||
} from "../types";
|
||||
import { check } from "src/server/queryApi";
|
||||
|
||||
type ConfigContextType = {
|
||||
isUpdateAvailable: boolean;
|
||||
releaseData: undefined | ReleaseGithubType;
|
||||
tokenMissing: boolean;
|
||||
config: undefined | ConfigParametersType;
|
||||
apiError: ApiReturnType | undefined;
|
||||
isConfigModalOpen: boolean;
|
||||
actions: {
|
||||
toggleModal: (isOpen: boolean) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const ConfigContext = React.createContext<ConfigContextType>(
|
||||
{} as ConfigContextType,
|
||||
);
|
||||
|
||||
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
|
||||
const [isConfigModalOpen, setIsConfigModalOpen] = useState<boolean>(false);
|
||||
const [tokenMissing, setTokenMissing] = useState(false);
|
||||
const [releaseData, setReleaseData] = useState<ReleaseGithubType>();
|
||||
const [apiError, setApiError] = useState<ApiReturnType>();
|
||||
const [config, setConfig] = useState<ConfigParametersType>();
|
||||
|
||||
// Open/close config modal
|
||||
const toggleModal = (isOpen: boolean) => {
|
||||
setIsConfigModalOpen(isOpen);
|
||||
};
|
||||
|
||||
// Check API
|
||||
const checkAPI = async () => {
|
||||
if (process.env.CI) {
|
||||
setTokenMissing(false);
|
||||
return;
|
||||
}
|
||||
const output: ApiReturnType | ConfigType = await check();
|
||||
if ((output as ApiReturnType)?.error) {
|
||||
setApiError(output as ApiReturnType);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = output as ConfigType;
|
||||
setTokenMissing(data?.noToken);
|
||||
setConfig(data.parameters);
|
||||
};
|
||||
|
||||
// Check Updates
|
||||
const checkForUpdates = async () => {
|
||||
console.log(config?.api.TIDARR_VERSION);
|
||||
if (config?.api.TIDARR_VERSION) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${config.api.TIDARR_REPO_URL}/releases`,
|
||||
);
|
||||
const data = (await response.json()) as ReleaseGithubType[];
|
||||
const latestVersion = data[0].tag_name.substring(
|
||||
1,
|
||||
data[0].tag_name.length,
|
||||
);
|
||||
const currentVersion = config.api.TIDARR_VERSION.substring(
|
||||
1,
|
||||
data[0].tag_name.length,
|
||||
);
|
||||
setIsUpdateAvailable(
|
||||
latestVersion !== currentVersion && latestVersion > currentVersion,
|
||||
);
|
||||
setReleaseData(data[0]);
|
||||
} catch (e) {
|
||||
console.log("fetch github issue", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkAPI();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkForUpdates();
|
||||
}, [config?.api.TIDARR_VERSION]);
|
||||
|
||||
const value = {
|
||||
isUpdateAvailable,
|
||||
releaseData,
|
||||
tokenMissing,
|
||||
config,
|
||||
apiError,
|
||||
isConfigModalOpen,
|
||||
actions: {
|
||||
toggleModal,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useConfigProvider = () => {
|
||||
return useContext(ConfigContext);
|
||||
};
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
ProcessingItemType,
|
||||
TrackType,
|
||||
} from "../types";
|
||||
import { check, list, remove, save } from "../server/queryApi";
|
||||
import { list, remove, save } from "../server/queryApi";
|
||||
|
||||
type ProcessingContextType = {
|
||||
processingList: ProcessingItemType[] | undefined;
|
||||
tokenMissing: boolean;
|
||||
apiError?: ApiReturnType;
|
||||
actions: {
|
||||
setProcessingList: (list: ProcessingItemType[]) => void;
|
||||
@@ -31,7 +30,6 @@ const ProcessingContext = React.createContext<ProcessingContextType>(
|
||||
|
||||
export function ProcessingProvider({ children }: { children: ReactNode }) {
|
||||
const [processingList, setProcessingList] = useState<ProcessingItemType[]>();
|
||||
const [tokenMissing, setTokenMissing] = useState(false);
|
||||
const [apiError, setApiError] = useState<ApiReturnType>();
|
||||
|
||||
// Add item to processing list
|
||||
@@ -76,6 +74,7 @@ export function ProcessingProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Retry failed processing item
|
||||
const retryItem = async (item: ProcessingItemType) => {
|
||||
if (
|
||||
processingList &&
|
||||
@@ -143,31 +142,12 @@ export function ProcessingProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Check API
|
||||
const checkAPI = async () => {
|
||||
if (process.env.CI) {
|
||||
setTokenMissing(false);
|
||||
return;
|
||||
}
|
||||
const output: ApiReturnType | { noToken: boolean; output: string } =
|
||||
await check();
|
||||
if ((output as ApiReturnType)?.error) {
|
||||
setApiError(output as ApiReturnType);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = output as { noToken: boolean; output: string };
|
||||
setTokenMissing(data?.noToken);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkAPI();
|
||||
updateFrontList();
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
processingList,
|
||||
tokenMissing,
|
||||
apiError,
|
||||
actions: {
|
||||
setProcessingList,
|
||||
|
||||
@@ -132,3 +132,20 @@ export type PlaylistType = {
|
||||
|
||||
lastItemAddedAt: string;
|
||||
};
|
||||
|
||||
export type ConfigType = {
|
||||
noToken: boolean;
|
||||
output: string;
|
||||
parameters: ConfigParametersType;
|
||||
};
|
||||
|
||||
export type ConfigParametersType = {
|
||||
api: { [key: string]: string };
|
||||
app: { [key: string]: string };
|
||||
};
|
||||
|
||||
export type ReleaseGithubType = {
|
||||
name: string;
|
||||
tag_name: string;
|
||||
body: string;
|
||||
};
|
||||
@@ -38,4 +38,19 @@ services:
|
||||
volumes:
|
||||
- .:/home/app/build
|
||||
ports:
|
||||
- 9323:9323
|
||||
- 9323:9323
|
||||
environment:
|
||||
- REACT_APP_TIDAL_COUNTRY_CODE=CA
|
||||
- REACT_APP_TIDAL_SEARCH_TOKEN=CzET4vdadNUFQ5JU
|
||||
- REACT_APP_TIDARR_SEARCH_URL=http://www.tidal.com/album/
|
||||
- TIDARR_REPO_URL=cstaelen/tidarr
|
||||
- TIDARR_VERSION=0.0.0
|
||||
- ENABLE_BEETS=true
|
||||
- ENABLE_PLEX_UPDATE=true
|
||||
- PLEX_URL=http://plex.url
|
||||
- PLEX_LIBRARY=3
|
||||
- PLEX_TOKEN=abc-plex-token-xyz
|
||||
- PLEX_PATH=/fodler/to/plex/music
|
||||
- ENABLE_GOTIFY=true
|
||||
- GOTIFY_URL=http://gotify.url
|
||||
- GOTIFY_TOKEN=abc-gotify-token-xyz
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,33 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/node": "^20.12.5",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"playwright:test": "playwright test",
|
||||
"playwright:test-update-snapshots": "playwright test --update-snapshots",
|
||||
"playwright:test-ui": "playwright test --ui",
|
||||
"playwright:test-chromium": "playwright test --project=chromium",
|
||||
"playwright:test-debug": "playwright test --debug",
|
||||
"playwright:codegen": "playwright codegen",
|
||||
"playwright:report": "playwright show-report"
|
||||
}
|
||||
"name": "tidarr-testing",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/node": "^20.12.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"prettier": "^3.2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"playwright:test": "playwright test",
|
||||
"playwright:test-update-snapshots": "playwright test --update-snapshots",
|
||||
"playwright:test-ui": "playwright test --ui",
|
||||
"playwright:test-chromium": "playwright test --project=chromium",
|
||||
"playwright:test-debug": "playwright test --debug",
|
||||
"playwright:codegen": "playwright codegen",
|
||||
"playwright:report": "playwright show-report",
|
||||
"find-deadcode": "ts-prune | (! grep -v 'used in module')",
|
||||
"depcheck": "npx depcheck",
|
||||
"eslint": "eslint .",
|
||||
"eslint-fix": "eslint --fix ."
|
||||
}
|
||||
}
|
||||
@@ -14,22 +14,22 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
workers: undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
[
|
||||
"html",
|
||||
process.env.CI ? "html" : "list",
|
||||
{
|
||||
host: "0.0.0.0",
|
||||
outputFolder: "./playwright-report",
|
||||
open: !process.env.CI,
|
||||
open: process.env.CI === undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
/* 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://localhost:8484/",
|
||||
baseURL: `http://localhost:${process.env.IS_DOCKER || process.env.CI ? 8484 : 3000}/`,
|
||||
locale: "en-US",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 37 KiB |
@@ -34,7 +34,87 @@ test("Tidarr config : Should see app version", async ({ page }) => {
|
||||
mockAPI(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator("span")).toContainText(
|
||||
`v${process.env.REACT_APP_TIDARR_VERSION}`,
|
||||
);
|
||||
await expect(page.locator(".footer span")).toContainText(`v0.0.0`);
|
||||
});
|
||||
|
||||
test("Tidarr config : Should see configuration dialog", async ({ page }) => {
|
||||
if (!process.env.IS_DOCKER) {
|
||||
mockAPI(page);
|
||||
}
|
||||
|
||||
await page.route("*/**/releases", async (route) => {
|
||||
const json = [
|
||||
{
|
||||
name: "0.0.0",
|
||||
tag_name: "0.0.0",
|
||||
},
|
||||
];
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
if (process.env.IS_DOCKER) {
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(".footer span").first()).toContainText(`v0.0.0`);
|
||||
|
||||
await page.getByRole("button", { name: "Configuration" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
|
||||
// Tab updates
|
||||
await expect(page.getByRole("tab", { name: "Updates" })).toBeVisible();
|
||||
await expect(page.getByText("Current version:")).toBeVisible();
|
||||
await expect(page.getByText("Tidarr is up to date.")).toBeVisible();
|
||||
|
||||
// Tab API
|
||||
await expect(page.getByRole("tab", { name: "API" })).toBeVisible();
|
||||
await page.getByRole("tab", { name: "API" }).click();
|
||||
await expect(
|
||||
page
|
||||
.locator("#alert-dialog-description div")
|
||||
.filter({ hasText: "Environment" }),
|
||||
).toHaveScreenshot();
|
||||
|
||||
// Tab APP
|
||||
await expect(page.getByRole("tab", { name: "Application" })).toBeVisible();
|
||||
await page.getByRole("tab", { name: "Application" }).click();
|
||||
await expect(
|
||||
page
|
||||
.locator("#alert-dialog-description div")
|
||||
.filter({ hasText: "Environment" }),
|
||||
).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Tidarr config : Should see update button", async ({ page }) => {
|
||||
await page.route("*/**/releases", async (route) => {
|
||||
const json = [
|
||||
{
|
||||
name: "0.0.8",
|
||||
tag_name: "0.0.8",
|
||||
},
|
||||
];
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
if (!process.env.IS_DOCKER) {
|
||||
mockAPI(page);
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
if (process.env.IS_DOCKER) {
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(".footer span").first()).toContainText(`v0.0.0`);
|
||||
|
||||
await expect(page.getByText("Update available")).toBeVisible();
|
||||
await page.getByText("Update available").click();
|
||||
// Tab updates
|
||||
await expect(page.getByRole("tab", { name: "Updates" })).toBeVisible();
|
||||
await expect(page.getByText("Current version: 0.0.0")).toBeVisible();
|
||||
await expect(page.getByText("Update available:")).toBeVisible();
|
||||
await expect(page.getByText("docker compose pull tidarr")).toBeVisible();
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import test, { Page, expect } from "@playwright/test";
|
||||
import { countItems, runSearch } from "./utils/search";
|
||||
import { waitForLoader } from "./utils/helpers";
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
async function testArtistSection(
|
||||
sectionIndex: number,
|
||||
@@ -18,6 +21,7 @@ async function testArtistSection(
|
||||
|
||||
if (count > 18) {
|
||||
await page.getByRole("button", { name: "LOAD MORE (page: 1/2)" }).click();
|
||||
await waitForLoader(page);
|
||||
|
||||
await countItems(
|
||||
`div > section.MuiContainer-root:nth-child(${sectionIndex + 1}) .MuiGrid-item`,
|
||||
@@ -26,22 +30,6 @@ async function testArtistSection(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test("Tidarr direct url : Should display artist page using Tidal artist url", async ({
|
||||
page,
|
||||
}) => {
|
||||
runSearch("https://listen.tidal.com/artist/17713", page);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Artist: Pennywise" }),
|
||||
).toBeVisible();
|
||||
|
||||
await testArtistSection(1, "Albums", 13, page);
|
||||
await testArtistSection(2, "EP & Singles", 2, page);
|
||||
await testArtistSection(3, "Compilations", 1, page);
|
||||
await testArtistSection(4, "Live albums", 1, page);
|
||||
await testArtistSection(5, "Appears On", 27, page);
|
||||
});
|
||||
|
||||
test("Tidarr direct url : Should display album result using Tidal album url", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -60,8 +48,25 @@ test("Tidarr direct url : Should display playlist result using Tidal playlist ur
|
||||
"https://listen.tidal.com/playlist/0b5df380-47d3-48fe-ae66-8f0dba90b1ee",
|
||||
page,
|
||||
);
|
||||
await waitForLoader(page);
|
||||
|
||||
await expect(page.getByRole("link", { name: "Grown Country" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Get playlist" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Tidarr direct url : Should display artist page using Tidal artist url", async ({
|
||||
page,
|
||||
}) => {
|
||||
runSearch("https://listen.tidal.com/artist/17713", page);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Artist: Pennywise" }),
|
||||
).toBeVisible();
|
||||
|
||||
await testArtistSection(1, "Albums", 13, page);
|
||||
await testArtistSection(2, "EP & Singles", 2, page);
|
||||
await testArtistSection(3, "Compilations", 1, page);
|
||||
await testArtistSection(4, "Live albums", 1, page);
|
||||
await testArtistSection(5, "Appears On", 27, page);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { runSearch, countItems } from "./utils/search";
|
||||
import { waitForImgLoaded, waitForLoader } from "./utils/helpers";
|
||||
|
||||
test("Tidarr search : Should see 'Top results' tab content", async ({
|
||||
page,
|
||||
@@ -52,7 +53,9 @@ test("Tidarr search : Should see 'Top results' tab content", async ({
|
||||
test("Tidarr search : Should see albums results", async ({ page }) => {
|
||||
await runSearch("Nirvana", page);
|
||||
await expect(page.locator("#full-width-tab-1")).toContainText("Albums (300)");
|
||||
|
||||
await page.getByRole("tab", { name: "Albums (300)" }).click();
|
||||
await waitForImgLoaded(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-1 .MuiGrid-item", 18, page);
|
||||
|
||||
@@ -64,11 +67,12 @@ test("Tidarr search : Should see albums results", async ({ page }) => {
|
||||
|
||||
await expect(
|
||||
page.locator("#full-width-tabpanel-1 .MuiGrid-item").first(),
|
||||
).toHaveScreenshot({ maxDiffPixels: 5 });
|
||||
).toHaveScreenshot({ maxDiffPixelRatio: 0.1 });
|
||||
|
||||
// Test pager
|
||||
|
||||
await page.getByRole("button", { name: "LOAD MORE (page: 1/17)" }).click();
|
||||
await waitForLoader(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-1 .MuiGrid-item", 36, page);
|
||||
});
|
||||
@@ -78,7 +82,9 @@ test("Tidarr search : Should see artists results", async ({ page }) => {
|
||||
await expect(page.locator("#full-width-tab-2")).toContainText(
|
||||
"Artists (100)",
|
||||
);
|
||||
|
||||
await page.getByRole("tab", { name: "Artists (100)" }).click();
|
||||
await waitForImgLoaded(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-2 .MuiGrid-item", 18, page);
|
||||
|
||||
@@ -90,11 +96,12 @@ test("Tidarr search : Should see artists results", async ({ page }) => {
|
||||
|
||||
await expect(
|
||||
page.locator("#full-width-tabpanel-2 .MuiGrid-item").first(),
|
||||
).toHaveScreenshot({ maxDiffPixels: 10 });
|
||||
).toHaveScreenshot({ maxDiffPixelRatio: 0.1 });
|
||||
|
||||
// Test pager
|
||||
|
||||
await page.getByRole("button", { name: "LOAD MORE (page: 1/6)" }).click();
|
||||
await waitForLoader(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-2 .MuiGrid-item", 36, page);
|
||||
|
||||
@@ -117,19 +124,21 @@ test("Tidarr search : Should see artists results", async ({ page }) => {
|
||||
.getByRole("button", { name: "Show discography" })
|
||||
.click();
|
||||
|
||||
await waitForLoader(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Artist: Nirvana" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.url()).toEqual(
|
||||
"http://localhost:8484/?query=artist:19368:Nirvana",
|
||||
);
|
||||
await expect(page.url()).toContain("/?query=artist:19368:Nirvana");
|
||||
});
|
||||
|
||||
test("Tidarr search : Should see tracks results", async ({ page }) => {
|
||||
await runSearch("Nirvana", page);
|
||||
await expect(page.locator("#full-width-tab-3")).toContainText("Tracks (300)");
|
||||
|
||||
await page.getByRole("tab", { name: "Tracks (300)" }).click();
|
||||
await waitForImgLoaded(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-3 .MuiGrid-item", 18, page);
|
||||
|
||||
@@ -146,6 +155,7 @@ test("Tidarr search : Should see tracks results", async ({ page }) => {
|
||||
// Test pager
|
||||
|
||||
await page.getByRole("button", { name: "LOAD MORE (page: 1/17)" }).click();
|
||||
await waitForLoader(page);
|
||||
|
||||
await countItems("#full-width-tabpanel-3 .MuiGrid-item", 36, page);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
export async function waitForLoader(page) {
|
||||
const loader = await page.getByTestId("loader")?.isVisible();
|
||||
if (loader) {
|
||||
await expect(page.getByTestId("loader")).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForImgLoaded(page) {
|
||||
await page.waitForFunction(() => {
|
||||
const images = Array.from(document.querySelectorAll("img"));
|
||||
return images.every((img) => img.complete);
|
||||
});
|
||||
}
|
||||
@@ -1,40 +1,31 @@
|
||||
import { Page } from "@playwright/test";
|
||||
// import { ProcessingItemType } from "../../../src/types";
|
||||
// let mockList: ProcessingItemType[] = [];
|
||||
|
||||
export async function mockAPI(page: Page) {
|
||||
await page.route("*/**/check", async (route) => {
|
||||
const json = {
|
||||
noToken: false,
|
||||
output: "",
|
||||
parameters: {
|
||||
api: {
|
||||
ENABLE_BEETS: "true",
|
||||
ENABLE_PLEX_UPDATE: "true",
|
||||
PLEX_URL: "http://plex.url",
|
||||
PLEX_LIBRARY: "3",
|
||||
PLEX_TOKEN: "abc-plex-token-xyz",
|
||||
PLEX_PATH: "/fodler/to/plex/music",
|
||||
ENABLE_GOTIFY: "true",
|
||||
GOTIFY_URL: "http://gotify.url",
|
||||
GOTIFY_TOKEN: "abc-gotify-token-xyz",
|
||||
TIDARR_VERSION: "0.0.0",
|
||||
TIDARR_REPO_URL: "cstaelen/tidarr",
|
||||
},
|
||||
app: {
|
||||
REACT_APP_TIDAL_SEARCH_TOKEN: "CzET4vdadNUFQ5JU",
|
||||
REACT_APP_TIDAL_COUNTRY_CODE: "CA",
|
||||
REACT_APP_TIDARR_SEARCH_URL: "http://www.tidal.com/album/",
|
||||
},
|
||||
},
|
||||
};
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
// await page.route("*/**/list", async (route) => {
|
||||
// const json = mockList;
|
||||
// await route.fulfill({ json });
|
||||
// });
|
||||
|
||||
// await page.route("*/**/remove", async (route) => {
|
||||
// mockList = [];
|
||||
// const json = mockList;
|
||||
// await route.fulfill({ json });
|
||||
// });
|
||||
|
||||
// await page.route("*/**/save", async (route) => {
|
||||
// const json: ProcessingItemType = {
|
||||
// id: "77610756",
|
||||
// artist: "Nirvana",
|
||||
// title: "Nevermind",
|
||||
// type: "album",
|
||||
// status: "processing",
|
||||
// loading: true,
|
||||
// error: false,
|
||||
// url: "http://www.tidal.com/album/77610756",
|
||||
// output: "\r\n=== Tidal-DL ===",
|
||||
// };
|
||||
// mockList.push(json);
|
||||
// await route.fulfill({ json });
|
||||
// });
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { mockAPI } from "./mock";
|
||||
import { waitForImgLoaded, waitForLoader } from "./helpers";
|
||||
|
||||
export async function runSearch(keyword: string, page: Page) {
|
||||
mockAPI(page);
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
await waitForImgLoaded(page);
|
||||
|
||||
await expect(page.getByRole("heading")).toContainText("Tidarr");
|
||||
await expect(
|
||||
page.getByLabel(
|
||||
@@ -16,6 +19,7 @@ export async function runSearch(keyword: string, page: Page) {
|
||||
await page.getByTestId("search-input").click();
|
||||
await page.getByTestId("search-input").fill(keyword);
|
||||
await page.getByTestId("search-input").press("Enter");
|
||||
await waitForLoader(page);
|
||||
}
|
||||
|
||||
export async function countItems(
|
||||
|
||||