feat: add updater (#13)

* feat: add updater

* ci: add tests
This commit is contained in:
cl.st authored and GitHub committed 2024-05-13 23:28:43 +02:00
1 parent ee7a76bc88
commit cd44432b05
38 files changed
+2456 -265

No files matched your search

+2 -1
View File
@@ -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=
+10 -10
View File
@@ -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 }}
+8
View File
@@ -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:
+1 -1
View File
@@ -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()
+6
View File
@@ -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
+8 -4
View File
@@ -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 🚀
+20
View File
@@ -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,
},
},
};
}
+1 -1
View File
@@ -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.
+25 -45
View File
@@ -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;
`;
+126
View File
@@ -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 />
&nbsp;
{"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>
);
};
+2 -2
View File
@@ -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;
+3
View File
@@ -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 (
+60
View File
@@ -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> • &nbsp;
{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>
)}
&nbsp;
<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;
`;
+110
View File
@@ -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);
};
+2 -22
View File
@@ -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,
+17
View File
@@ -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;
};
+16 -1
View File
@@ -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
+16
View File
@@ -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"
}
}
+1830 -100
View File
File diff suppressed because it is too large. Load diff
+31 -21
View File
@@ -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 ."
}
}
+4 -4
View File
@@ -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 */
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+83 -3
View File
@@ -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();
});
+21 -16
View File
@@ -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);
});
+15 -5
View File
@@ -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);
});
+15
View File
@@ -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);
});
}
+20 -29
View File
@@ -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 });
// });
}
+4
View File
@@ -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(