diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d35079d..0208652 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,10 +1,12 @@ name: Playwright Tests on: push: - branches-ignore: [ gh-pages ] + branches: [ main ] + pull_request: + branches: [ main ] concurrency: group: ${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: test: timeout-minutes: 60 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1716a45..756f5d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ Shazarr notable changes. [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) format. +## 📦 0.3.0 +### 🖍 Changed +* Results shows now as a inline card instead of a dedicated screen. +* Enhance UI for landscape mode + ## 📦 0.2.1 ### 🐛 Fixed * Fix Tidarr network error diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 6406b00..5f72368 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -1,30 +1,30 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/android/capacitor') +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor') include ':capacitor-app' -project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app/android') +project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/app/android') include ':capacitor-device' -project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/device/android') +project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.4/node_modules/@capacitor/device/android') include ':capacitor-filesystem' -project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1/node_modules/@capacitor/filesystem/android') +project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.4/node_modules/@capacitor/filesystem/android') include ':capacitor-haptics' -project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/haptics/android') +project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.4/node_modules/@capacitor/haptics/android') include ':capacitor-local-notifications' -project(':capacitor-local-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+local-notifications@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/local-notifications/android') +project(':capacitor-local-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+local-notifications@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/local-notifications/android') include ':capacitor-network' -project(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/network/android') +project(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/network/android') include ':capacitor-preferences' -project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/preferences/android') +project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/preferences/android') include ':capacitor-screen-orientation' -project(':capacitor-screen-orientation').projectDir = new File('../node_modules/.pnpm/@capacitor+screen-orientation@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/screen-orientation/android') +project(':capacitor-screen-orientation').projectDir = new File('../node_modules/.pnpm/@capacitor+screen-orientation@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/screen-orientation/android') include ':capacitor-voice-recorder' -project(':capacitor-voice-recorder').projectDir = new File('../node_modules/.pnpm/capacitor-voice-recorder@7.0.6_@capacitor+core@8.3.1_@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1_/node_modules/capacitor-voice-recorder/android') +project(':capacitor-voice-recorder').projectDir = new File('../node_modules/.pnpm/capacitor-voice-recorder@7.0.6_@capacitor+core@8.3.4_@capacitor+filesystem@8.1.2_@capacitor+core@8.3.4_/node_modules/capacitor-voice-recorder/android') diff --git a/dist/index.html b/dist/index.html index 1116db2..2255603 100644 --- a/dist/index.html +++ b/dist/index.html @@ -24,7 +24,7 @@ })(); - + diff --git a/package.json b/package.json index b150e86..22c0be3 100644 --- a/package.json +++ b/package.json @@ -66,11 +66,11 @@ "quality:depcheck": "npx depcheck", "quality:eslint": "eslint .", "quality:eslint-fix": "eslint --fix .", - "testing:run": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test'", + "testing:run": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test'", "testing:ui": "npx playwright test --ui", - "testing:ci": "docker run --rm -e CI=true -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test'", - "testing:show-report": "docker run --rm -it -p 9323:9323 -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble npx playwright show-report ./testing/playwright-report --host 0.0.0.0", - "testing:update-snapshots": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.59.1-noble sh -c 'npm i -g pnpm && pnpm install && npx playwright test --update-snapshots'" + "testing:ci": "docker run --rm -e CI=true -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test'", + "testing:show-report": "docker run --rm -it -p 9323:9323 -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble npx playwright show-report ./testing/playwright-report --host 0.0.0.0", + "testing:update-snapshots": "docker run --rm -it -v .:/srv -w /srv mcr.microsoft.com/playwright:v1.60.0-noble sh -c 'npm i -g pnpm@10.28.2 && pnpm install --no-frozen-lockfile && npx playwright test --update-snapshots'" }, "pnpm": { "overrides": { diff --git a/src/App.tsx b/src/App.tsx index 60157eb..7c8dcf6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,9 +11,9 @@ import { Typography, } from "@mui/material"; -import Config from "./components/Config"; +import { AlertHandler } from "./components/Common/Alert/AlertHandler"; +import BottomActions from "./components/Common/BottomActions"; import { ConfigProvider } from "./components/Config/Provider"; -import HistoryList from "./components/History/List"; import { HistoryProvider } from "./components/History/Provider"; import ShazarrButton from "./components/Shazarr/Button"; import { ShazarrProvider } from "./components/Shazarr/Provider"; @@ -43,7 +43,7 @@ function App() {
- +

Shazarr

@@ -63,12 +63,10 @@ function App() { >
- - - - +
+
diff --git a/src/components/Config/Alert.tsx b/src/components/Common/Alert/Alert.tsx similarity index 87% rename from src/components/Config/Alert.tsx rename to src/components/Common/Alert/Alert.tsx index 5f9af1a..4ff2c08 100644 --- a/src/components/Config/Alert.tsx +++ b/src/components/Common/Alert/Alert.tsx @@ -1,6 +1,6 @@ import { Alert, Box } from "@mui/material"; -import errorCode, { ErrorCodeType } from "./errorCode"; +import errorCode, { ErrorCodeType } from "../../Config/errorCode"; export default function ApiErrorAlert({ message, diff --git a/src/components/Common/Alert/AlertHandler.tsx b/src/components/Common/Alert/AlertHandler.tsx new file mode 100644 index 0000000..1bb8a7c --- /dev/null +++ b/src/components/Common/Alert/AlertHandler.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { Box } from "@mui/material"; + +import { useConfigProvider } from "../../Config/useConfig"; +import { useHistoryProvider } from "../../History/useHistory"; +import ShazarrResults from "../../Shazarr/Result"; +import InlineResultCard from "../../Shazarr/ui/InlineResultCard"; +import { useShazarrProvider } from "../../Shazarr/useShazarr"; + +import ApiErrorAlert from "./Alert"; + +export const AlertHandler = () => { + const [resultOpen, setResultOpen] = useState(false); + const { recordingStatus, recordingError, showInlineResult, actions: { dismissInlineResult } } = useShazarrProvider(); + const { isNetworkConnected } = useConfigProvider(); + const { history } = useHistoryProvider(); + + const lastItem = history?.[history.length - 1]; + + function handleDismiss() { + dismissInlineResult(); + setResultOpen(false); + } + + function handleOpenResult() { + dismissInlineResult(); + setResultOpen(true); + } + + return ( + + setResultOpen(false)} + /> + {showInlineResult && lastItem?.data && recordingStatus === "inactive" && ( + + )} + {!isNetworkConnected && recordingStatus === "inactive" && ( + + )} + {recordingError && recordingStatus === "inactive" && ( + + )} + + ); +}; diff --git a/src/components/Common/BottomActions.tsx b/src/components/Common/BottomActions.tsx new file mode 100644 index 0000000..e358894 --- /dev/null +++ b/src/components/Common/BottomActions.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import styled from "@emotion/styled"; +import { List, Settings } from "@mui/icons-material"; +import { Box, Fab, useMediaQuery } from "@mui/material"; + +import Config from "../Config"; +import { useConfigProvider } from "../Config/useConfig"; +import HistoryList from "../History/List"; +import { useHistoryProvider } from "../History/useHistory"; +import { useShazarrProvider } from "../Shazarr/useShazarr"; + +export default function BottomActions() { + const [configOpen, setConfigOpen] = useState(false); + const [historyOpen, setHistoryOpen] = useState(false); + const { recordingStatus } = useShazarrProvider(); + const { formConfig } = useConfigProvider(); + const { history } = useHistoryProvider(); + const isLandscape = useMediaQuery("(orientation: landscape)"); + + const disabled = recordingStatus !== "inactive"; + + return ( + <> + setConfigOpen(false)} /> + setHistoryOpen(false)} /> + + + {formConfig && ( + setConfigOpen(true)} + disabled={disabled} + aria-label="Configuration" + > + + + )} + {history && history.length > 0 && ( + setHistoryOpen(true)} + disabled={disabled} + aria-label="Records" + > + + + )} + + + ); +} + +const ActionsContainer = styled(Box)<{ landscape: boolean }>` + position: fixed; + z-index: 1000; + display: flex; + gap: 24px; + + ${({ landscape }) => + landscape + ? ` + right: 12px; + top: 50%; + transform: translateY(-50%); + flex-direction: column; + ` + : ` + bottom: 40px; + left: 0; + right: 0; + justify-content: center; + flex-direction: row; + `} +`; diff --git a/src/components/Config/Form.tsx b/src/components/Config/Form.tsx deleted file mode 100644 index 07c973c..0000000 --- a/src/components/Config/Form.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { FormEvent, useState } from "react"; -import styled from "@emotion/styled"; -import { - Box, - Button, - Dialog, - DialogContent, - Input, - Link, - Paper, -} from "@mui/material"; - -import { useShazarrProvider } from "../Shazarr/useShazarr"; - -import { useConfigProvider } from "./useConfig"; - -export default function ConfigForm() { - const [showApiForm, setShowApiForm] = useState(); - const { recordingStatus } = useShazarrProvider(); - const { - config, - formConfig, - actions: { setConfig }, - } = useConfigProvider(); - - if (!formConfig) return null; - - function saveConfig(e: FormEvent) { - e.preventDefault(); - const data = new FormData(e.currentTarget); - setConfig({ - ...config, - lidarr_url: data.get("lidarr_url")?.toString() || "", - lidarr_api_key: data.get("lidarr_api_key")?.toString() || "", - tidarr_url: data.get("tidarr_url")?.toString() || "", - tidarr_api_key: data.get("tidarr_api_key")?.toString() || "", - custom_service_url: data.get("custom_service_url")?.toString() || "", - custom_service_name: data.get("custom_service_name")?.toString() || "", - }); - setShowApiForm(false); - } - - return ( - <> - setShowApiForm(false)} - > - - saveConfig(e)}> - {Object.entries(formConfig)?.map((field, index) => ( - - - - - - ))} - - - Github page -{" "} - {import.meta.env.VITE_STAGE === "testing" || import.meta.env.MODE === "development" - ? "v0.0.0" - : import.meta.env.VITE_CURRENT_VERSION} - - - - - - - - - - - - - ); -} - -const ButtonConfig = styled.div` - margin-bottom: 1rem; -`; diff --git a/src/components/Config/index.tsx b/src/components/Config/index.tsx index ec91cec..b6a144a 100644 --- a/src/components/Config/index.tsx +++ b/src/components/Config/index.tsx @@ -1,25 +1,102 @@ +import { FormEvent } from "react"; +import { + Box, + Button, + Drawer, + Input, + Link, + Paper, +} from "@mui/material"; + import { useShazarrProvider } from "../Shazarr/useShazarr"; -import ApiErrorAlert from "./Alert"; -import ConfigForm from "./Form"; import { useConfigProvider } from "./useConfig"; -export default function Config() { - const { recordingStatus, recordingError } = useShazarrProvider(); - const { formConfig, isNetworkConnected } = useConfigProvider(); +interface Props { + open: boolean; + onClose: () => void; +} + +export default function Config({ open, onClose }: Props) { + const { recordingStatus } = useShazarrProvider(); + const { + config, + formConfig, + actions: { setConfig }, + } = useConfigProvider(); if (!formConfig) return null; - return ( - <> - {!isNetworkConnected && recordingStatus === "inactive" && ( - - )} - {recordingError && recordingStatus === "inactive" && ( - - )} + function saveConfig(e: FormEvent) { + e.preventDefault(); + const data = new FormData(e.currentTarget); + setConfig({ + ...config, + lidarr_url: data.get("lidarr_url")?.toString() || "", + lidarr_api_key: data.get("lidarr_api_key")?.toString() || "", + tidarr_url: data.get("tidarr_url")?.toString() || "", + tidarr_api_key: data.get("tidarr_api_key")?.toString() || "", + custom_service_url: data.get("custom_service_url")?.toString() || "", + custom_service_name: data.get("custom_service_name")?.toString() || "", + }); + onClose(); + } - - + return ( + + saveConfig(e)} + sx={{ + display: "flex", + alignSelf: "center", + flexDirection: "column", + padding: 2, + maxWidth: 480, + width: "100%", + }}> + {Object.entries(formConfig)?.map((field, index) => ( + + + + + + ))} + + + Github page -{" "} + {import.meta.env.VITE_STAGE === "testing" || import.meta.env.MODE === "development" + ? "v0.0.0" + : import.meta.env.VITE_CURRENT_VERSION} + + + + + + + ); } diff --git a/src/components/History/Card.tsx b/src/components/History/Card.tsx index 0d8cc09..2333fd2 100644 --- a/src/components/History/Card.tsx +++ b/src/components/History/Card.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { DeleteForever, RemoveRedEye, Search } from "@mui/icons-material"; import { ButtonBase, CardActions, CardMedia, IconButton } from "@mui/material"; import Card from "@mui/material/Card"; @@ -5,6 +6,7 @@ import CardContent from "@mui/material/CardContent"; import Typography from "@mui/material/Typography"; import { ImageWithFallback } from "../Common/ImageWithFallback"; +import ShazarrResults from "../Shazarr/Result"; import { useShazarrProvider } from "../Shazarr/useShazarr"; import { HistoryItem } from "./context"; @@ -17,8 +19,9 @@ export default function HistoryCard({ item: HistoryItem; onClose: () => void; }) { + const [resultOpen, setResultOpen] = useState(false); const { - actions: { setShazarrResponse, searchOfflineRecord }, + actions: { searchOfflineRecord }, } = useShazarrProvider(); const { actions: { deleteHistoryItem }, @@ -29,7 +32,7 @@ export default function HistoryCard({ function handleClickItem() { if (item?.data) { - setShazarrResponse(item.data); + setResultOpen(true); } else { searchOfflineRecord(item); } @@ -37,43 +40,45 @@ export default function HistoryCard({ } return ( - - - - - - - - - - {item.title} {`- ${item.artist}`} -
- - {dateRecord} - -
-
-
- - - {item?.data ? : } - - { - deleteHistoryItem(item.date); - }} - > - - - -
+ <> + setResultOpen(false)} + /> + + + + + + + + + + {item.title} {`- ${item.artist}`} +
+ + {dateRecord} + +
+
+
+ + + {item?.data ? : } + + deleteHistoryItem(item.date)}> + + + +
+ ); } diff --git a/src/components/History/List.tsx b/src/components/History/List.tsx index ccac935..69eaaf9 100644 --- a/src/components/History/List.tsx +++ b/src/components/History/List.tsx @@ -1,63 +1,53 @@ -import { useState } from "react"; -import { Close, List } from "@mui/icons-material"; -import { Box, Button, Container, Drawer, useTheme } from "@mui/material"; - -import { useShazarrProvider } from "../Shazarr/useShazarr"; +import { Close } from "@mui/icons-material"; +import { Box, Button, Container, Drawer, useMediaQuery, useTheme } from "@mui/material"; import HistoryCard from "./Card"; import { useHistoryProvider } from "./useHistory"; -export default function HistoryList() { - const { recordingStatus } = useShazarrProvider(); +interface Props { + open: boolean; + onClose: () => void; +} + +export default function HistoryList({ open, onClose }: Props) { + const isLandscape = useMediaQuery("(orientation: landscape)"); const { history } = useHistoryProvider(); - const [listOpen, setListOpen] = useState(); const { palette } = useTheme(); if (!history || history?.length === 0) return null; return ( - - - setListOpen(false)} - ModalProps={{ - keepMounted: true, - }} - > - - - {history?.map((item, index) => ( - setListOpen(false)} - /> - ))} - - - - - + + {history?.map((item, index) => ( + + ))} + + + + ); } diff --git a/src/components/Shazarr/Button.tsx b/src/components/Shazarr/Button.tsx index ad42591..4fc731e 100644 --- a/src/components/Shazarr/Button.tsx +++ b/src/components/Shazarr/Button.tsx @@ -1,4 +1,3 @@ -import React from "react"; import styled from "@emotion/styled"; import { MoreHorizOutlined } from "@mui/icons-material"; import GraphicEqIcon from "@mui/icons-material/GraphicEq"; @@ -16,56 +15,53 @@ export default function ShazarrButton() { shazarrLoading, shazarrResponse, recordingStatus, - actions: { setRecordingStatus, resetSearch }, + actions: { startRecording, resetSearch }, } = useShazarrProvider(); return ( - + {recordingStatus !== "inactive" ? ( ) : null} - {!shazarrResponse ? ( - <> - - { - resetSearch(); - if (recordingStatus === "inactive") { - setRecordingStatus("start"); - } - }} - > - - {recordingStatus === "searching" ? ( - - ) : recordingStatus === "recording" ? ( - - ) : ( - <> - - - )} - - - - - ) : ( - - )} + + + { + resetSearch(); + if (recordingStatus === "inactive") { + startRecording(); + } + }} + > + + {recordingStatus === "searching" ? ( + + ) : recordingStatus === "recording" ? ( + + ) : ( + <> + + + )} + + + + {shazarrResponse && } ); } diff --git a/src/components/Shazarr/Provider.tsx b/src/components/Shazarr/Provider.tsx index 8e0eb1f..17378a8 100644 --- a/src/components/Shazarr/Provider.tsx +++ b/src/components/Shazarr/Provider.tsx @@ -1,12 +1,14 @@ -import { ReactNode, useCallback, useEffect, useState } from "react"; +import { ReactNode, useState } from "react"; import { Haptics } from "@capacitor/haptics"; import { ScreenOrientation } from "@capacitor/screen-orientation"; import { VoiceRecorder } from "capacitor-voice-recorder"; import { Shazam } from "shazam-api"; -import { ShazamRoot, ShazamTrack } from "shazam-api/dist/types"; +import { ShazamRoot } from "shazam-api/dist/types"; import { RECORD_DURATION } from "../../constant"; +import { ErrorCodeType } from "../Config/errorCode"; import { useConfigProvider } from "../Config/useConfig"; +import { HistoryItem } from "../History/context"; import { useHistoryProvider } from "../History/useHistory"; import { mockRecordBase64 } from "./mock/recordBase64"; @@ -17,21 +19,16 @@ export type { RecordingStatusType } from "./context"; export function ShazarrProvider({ children }: { children: ReactNode }) { const [audio, setAudio] = useState(); - const [shazarrResponse, setShazarrResponse] = useState(); + const [showInlineResult, setShowInlineResult] = useState(false); const [shazarrLoading, setShazarrLoading] = useState(false); - const [recordingError, setRecordingError] = useState< - ReturnType["isNetworkConnected"] extends boolean - ? never - : never - >(); + const [recordingError, setRecordingError] = useState(); const [recordingStatus, setRecordingStatus] = useState("inactive"); - const [historySearch, setHistorySearch] = useState(); - const [cleanHistorySearch, setCleanHistorySearch] = useState(); const { isNetworkConnected } = useConfigProvider(); + const { - actions: { addItemToHistory, deleteHistoryItem }, + actions: { addItemToHistory }, } = useHistoryProvider(); const vibrateAction = async () => { @@ -42,12 +39,15 @@ export function ShazarrProvider({ children }: { children: ReactNode }) { } }; - const resetSearch = useCallback(async () => { - setShazarrResponse(undefined); + const dismissInlineResult = () => { + setShowInlineResult(false); + }; + + const resetSearch = async () => { + setShowInlineResult(false); setAudio(undefined); setRecordingStatus("inactive"); setShazarrLoading(false); - setHistorySearch(undefined); await ScreenOrientation.unlock(); @@ -59,185 +59,146 @@ export function ShazarrProvider({ children }: { children: ReactNode }) { } catch (e) { console.error(e); } - }, []); + }; - const processRecording = useCallback( - async (duration: number = RECORD_DURATION) => { - setRecordingError(undefined); - try { - const { value: hasPerm } = - await VoiceRecorder.hasAudioRecordingPermission(); - if (!hasPerm) { - const { value: resultRequestPerm } = - await VoiceRecorder.requestAudioRecordingPermission(); + function handleTesting(date: number) { + const audioData = mockRecordBase64; + setAudio(audioData); + if (!isNetworkConnected) { + addItemToHistory({ + title: "Offline record", + artist: "Not discovered", + date: date, + stream: audioData, + }); + setRecordingStatus("inactive"); + return; + } + setRecordingStatus("searching"); + processTranscoding(audioData); + } - if (!resultRequestPerm) return; + const processShazam = async (samples: number[]) => { + const shazam = new Shazam(); + + shazam + .fullRecognizeSong(samples) + .then((value: ShazamRoot | null) => { + console.log("shazarr:", value); + const response = value?.track; + if (response?.title) { + addItemToHistory({ + title: response.title, + artist: response.subtitle, + date: Date.now(), + data: response, + }); + setShowInlineResult(true); + } else { + setRecordingError("SHAZARR_NOT_FOUND" as never); } + }) + .catch((err) => { + console.error("shazarr:", err); + setRecordingError("SHAZAM_API_ERROR" as never); + }) + .finally(async () => { + await vibrateAction(); + await ScreenOrientation.unlock(); + setRecordingStatus("inactive"); + setShazarrLoading(false); + }); + }; - setRecordingStatus("recording"); - vibrateAction(); - const { value: isRecording } = await VoiceRecorder.startRecording(); + const processTranscoding = (recordDataBase64: string) => { + setShazarrLoading(true); + transcodePCM16(recordDataBase64).then((pcm16) => { + processShazam(pcm16); + }); + }; - if (isRecording) { - setTimeout(async () => { - const { status } = await VoiceRecorder.getCurrentStatus(); - if (recordingStatus !== "inactive" && status === "RECORDING") { - const { - value: { recordDataBase64 }, - } = await VoiceRecorder.stopRecording(); + const startRecording = async (duration: number = RECORD_DURATION) => { + setRecordingError(undefined); + try { + const { value: hasPerm } = + await VoiceRecorder.hasAudioRecordingPermission(); - if (import.meta.env.VITE_STAGE === "testing") { - setAudio(mockRecordBase64); - } else { - setAudio(recordDataBase64); - } + if (!hasPerm) { + const { value: resultRequestPerm } = + await VoiceRecorder.requestAudioRecordingPermission(); - setRecordingStatus("searching"); - await vibrateAction(); - } - }, duration); - } - } catch (err) { - console.error(err); - if (import.meta.env.VITE_STAGE === "testing") { - setAudio(mockRecordBase64); - setRecordingStatus("searching"); - return; - } - resetSearch(); - setRecordingError("ERROR_RECORDING" as never); + if (!resultRequestPerm) return; } - }, - [recordingStatus, resetSearch], - ); - const processShazam = useCallback( - async (samples: number[]) => { - const shazam = new Shazam(); + vibrateAction(); + setRecordingStatus("recording"); - shazam - .fullRecognizeSong(samples) - .then((value: ShazamRoot | null) => { - const response = value?.track; - if (response?.title && recordingStatus !== "inactive") { - setShazarrResponse(response); + const { value: isRecording } = await VoiceRecorder.startRecording(); - addItemToHistory({ - title: response.title, - artist: response.subtitle, - date: Date.now(), - data: response, - }); + if (isRecording) { + setTimeout(async () => { + const { + value: { recordDataBase64 }, + } = await VoiceRecorder.stopRecording(); - if (historySearch) { - setCleanHistorySearch(historySearch); - } - } else { - setShazarrResponse(undefined); - setRecordingError("SHAZARR_NOT_FOUND" as never); - } - }) - .catch((err) => { - console.error(err); - if (!historySearch) { + const audioData = import.meta.env.VITE_STAGE === "testing" + ? mockRecordBase64 + : recordDataBase64; + + if (!audioData) return; + + setAudio(audioData); + setRecordingStatus("searching"); + await vibrateAction(); + + if (!isNetworkConnected) { addItemToHistory({ title: "Offline record", artist: "Not discovered", date: Date.now(), - stream: audio, + stream: audioData, }); + setRecordingStatus("inactive"); + return; } - setRecordingError("SHAZAM_API_ERROR" as never); - }) - .finally(async () => { - await vibrateAction(); - await ScreenOrientation.unlock(); - setRecordingStatus("inactive"); - setShazarrLoading(false); - }); - }, - [addItemToHistory, audio, historySearch, recordingStatus], - ); - const processTranscoding = useCallback(() => { - setShazarrLoading(true); + processTranscoding(audioData); + + }, duration); + } + } catch (err) { + console.error(err); - if (!audio || audio.length === 0) return; + if (import.meta.env.VITE_STAGE === "testing") { + handleTesting(Date.now()); + return; + }; - if (!isNetworkConnected && !historySearch) { - addItemToHistory({ - title: "Offline record", - artist: "Not discovered", - date: Date.now(), - stream: audio, - }); - vibrateAction(); - setRecordingStatus("inactive"); - setShazarrLoading(false); - } else { - transcodePCM16(audio).then((pcm16) => { - if (recordingStatus !== "inactive") { - processShazam(pcm16); - } - }); - } - }, [ - addItemToHistory, - audio, - historySearch, - isNetworkConnected, - processShazam, - recordingStatus, - ]); - - const searchOfflineRecord = (item: Parameters[0]) => { - if (item?.stream) { resetSearch(); - setAudio(item.stream); - setHistorySearch(item.date); - setRecordingStatus("searching"); - } + setRecordingError("ERROR_RECORDING" as never); + }; }; - useEffect(() => { - if (shazarrResponse) return; - - switch (recordingStatus) { - case "start": - // eslint-disable-next-line react-hooks/set-state-in-effect - processRecording(); - break; - case "searching": - if (audio) { - processTranscoding(); - } - break; - } - }, [ - audio, - processRecording, - processTranscoding, - recordingStatus, - shazarrResponse, - ]); - - useEffect(() => { - if (cleanHistorySearch) { - deleteHistoryItem(cleanHistorySearch); - } - }, [cleanHistorySearch, deleteHistoryItem]); + const searchOfflineRecord = (item: HistoryItem) => { + if (!item?.stream) return; + + resetSearch(); + setAudio(item.stream); + setRecordingStatus("searching"); + processTranscoding(item.stream); + }; const value = { recordingError, shazarrLoading, - shazarrResponse, + showInlineResult, recordingStatus, audio, actions: { - setRecordingStatus, + startRecording, resetSearch, - setShazarrResponse, + dismissInlineResult, searchOfflineRecord, }, }; diff --git a/src/components/Shazarr/Result.tsx b/src/components/Shazarr/Result.tsx index 1e2be81..65e4695 100644 --- a/src/components/Shazarr/Result.tsx +++ b/src/components/Shazarr/Result.tsx @@ -1,91 +1,123 @@ +import { useMemo } from "react"; import { Close } from "@mui/icons-material"; -import { Box, Button, Divider, Stack } from "@mui/material"; -import { ShazamProvider } from "shazam-api/dist/types"; +import { Box, Button, Divider, Drawer, Stack, Table, TableBody, TableCell, TableContainer, TableRow, useMediaQuery } from "@mui/material"; +import { ShazamProvider, ShazamTrack } from "shazam-api/dist/types"; import { useConfigProvider } from "../Config/useConfig"; +import ButtonLyrics from "./ui/ButtonLyrics"; import CardResult from "./ui/Card"; import CustomServiceButton from "./ui/CustomServiceButton"; import LidarrButton from "./ui/LidarrButton"; import StreamProviderButton from "./ui/StreamProviderButton"; import TidarrButton from "./ui/TidarrButton"; -import { useShazarrProvider } from "./useShazarr"; -export default function ShazarrResults() { - const { - shazarrResponse, - actions: { resetSearch }, - } = useShazarrProvider(); +interface Props { + data: ShazamTrack | undefined; + onClose: () => void; +} +export default function ShazarrResults({ data, onClose }: Props) { + const isLandscape = useMediaQuery("(orientation: landscape)"); const { config } = useConfigProvider(); - const albumName = shazarrResponse?.sections?.[0]?.metadata?.filter( + const albumName = data?.sections?.[0]?.metadata?.filter( (m: { title: string }) => m?.title === "Album", )?.[0]?.text; - if (!shazarrResponse) return null; + const lyrics = useMemo( + () => data?.sections?.filter((section) => section.type === "LYRICS")?.[0]?.text, + [data], + ); + + if (!data) return null; return ( - <> - -
- - {config?.lidarr_url && ( - - )} - {config?.tidarr_url && ( - - )} - {config?.custom_service_url && config?.custom_service_name && ( - - )} - - - - {shazarrResponse?.hub?.providers?.map( - (provider: ShazamProvider, index: number) => ( - - ), - )} - - {shazarrResponse?.myshazam?.apple?.actions?.[0]?.uri && ( - - )} + + + + + + + + + + {data?.sections?.[0]?.metadata?.map((row, index) => ( + + + {row.title} + + {row.text} + + ))} + +
+
- + - - -
- + {config?.lidarr_url && ( + + )} + {config?.tidarr_url && ( + + )} + {config?.custom_service_url && config?.custom_service_name && ( + + )} + + + + {data?.hub?.providers?.map( + (provider: ShazamProvider, index: number) => ( + + ), + )} + {data?.myshazam?.apple?.actions?.[0]?.uri && ( + + )} + + + + + +
+
+ + ); } diff --git a/src/components/Shazarr/context.ts b/src/components/Shazarr/context.ts index b7544d7..253b6f1 100644 --- a/src/components/Shazarr/context.ts +++ b/src/components/Shazarr/context.ts @@ -1,26 +1,21 @@ import { createContext } from "react"; -import { ShazamTrack } from "shazam-api/dist/types"; import { ErrorCodeType } from "../Config/errorCode"; import { HistoryItem } from "../History/context"; -export type RecordingStatusType = - | "start" - | "recording" - | "searching" - | "inactive"; +export type RecordingStatusType = "recording" | "searching" | "inactive"; type ShazarrContextType = { recordingError: ErrorCodeType | undefined; shazarrLoading: boolean; - shazarrResponse: ShazamTrack | undefined; + showInlineResult: boolean; recordingStatus: RecordingStatusType; audio: string | undefined; actions: { - setRecordingStatus: (status: RecordingStatusType) => void; - setShazarrResponse: (data: ShazamTrack) => void; + startRecording: () => void; searchOfflineRecord: (item: HistoryItem) => void; resetSearch: () => void; + dismissInlineResult: () => void; }; }; diff --git a/src/components/Shazarr/ui/ButtonLyrics.tsx b/src/components/Shazarr/ui/ButtonLyrics.tsx new file mode 100644 index 0000000..4382ca6 --- /dev/null +++ b/src/components/Shazarr/ui/ButtonLyrics.tsx @@ -0,0 +1,29 @@ +import { ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from "@mui/material"; + +export default function ButtonLyrics({ lyrics }: { lyrics: string[] | undefined }) { + if (!lyrics || lyrics?.length === 0) return null; + return ( + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + LYRICS + + + + {lyrics?.map((lyric, index) => ( + + {lyric} +
+
+ ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Shazarr/ui/Card.tsx b/src/components/Shazarr/ui/Card.tsx index e99f2a5..08ae146 100644 --- a/src/components/Shazarr/ui/Card.tsx +++ b/src/components/Shazarr/ui/Card.tsx @@ -1,23 +1,14 @@ import { useEffect, useMemo, useState } from "react"; import { App } from "@capacitor/app"; import { - ExpandMore, PauseCircleFilled, PlayCircleFilled, Refresh, } from "@mui/icons-material"; import { - Accordion, - AccordionDetails, - AccordionSummary, Button, CardActions, CardMedia, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, } from "@mui/material"; import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; @@ -26,16 +17,10 @@ import Typography from "@mui/material/Typography"; import { ShazamTrack } from "shazam-api/dist/types"; import { ImageWithFallback } from "../../Common/ImageWithFallback"; -import { useShazarrProvider } from "../useShazarr"; -export default function CardResult({ data }: { data: ShazamTrack }) { +export default function CardResult({ data, onClose }: { data: ShazamTrack; onClose: () => void }) { const [isPlaying, setIsPlaying] = useState(); - const lyrics = useMemo( - () => data?.sections?.filter((section) => section.type === "LYRICS")?.[0]?.text, - [data], - ); - const sampleURI = useMemo( () => data?.hub?.actions?.filter((action) => !!action.uri)?.[0]?.uri, [data], @@ -45,9 +30,6 @@ export default function CardResult({ data }: { data: ShazamTrack }) { () => (sampleURI ? new Audio(sampleURI) : undefined), [sampleURI], ); - const { - actions: { resetSearch }, - } = useShazarrProvider(); function togglePlayPause() { if (!sample) return; @@ -79,95 +61,54 @@ export default function CardResult({ data }: { data: ShazamTrack }) { }, [sample]); return ( - <> - - - - - - - {data.title} - - - {data.subtitle} - - - {sample && ( - - + + + + + + {data.title} + + + {data.subtitle} + + + {sample && ( + + + -   - - - - )} - - - - - - {data?.sections?.[0]?.metadata?.map((row, index) => ( - - - {row.title} - - {row.text} - - ))} - -
-
- - {lyrics && lyrics?.length > 0 && ( - - - } - aria-controls="panel1a-content" - id="panel1a-header" + {isPlaying ? "Pause" : "Play"} + +   + + + )} - + ); } diff --git a/src/components/Shazarr/ui/InlineResultCard.tsx b/src/components/Shazarr/ui/InlineResultCard.tsx new file mode 100644 index 0000000..a59cbda --- /dev/null +++ b/src/components/Shazarr/ui/InlineResultCard.tsx @@ -0,0 +1,46 @@ +import { Close, RemoveRedEye } from "@mui/icons-material"; +import { Box, ButtonBase, Card, CardActions, CardContent, CardMedia, IconButton, Typography } from "@mui/material"; +import { ShazamTrack } from "shazam-api/dist/types"; + +import { ImageWithFallback } from "../../Common/ImageWithFallback"; + +interface Props { + track: ShazamTrack; + onDismiss: () => void; + onOpen: () => void; +} + +export default function InlineResultCard({ track, onDismiss, onOpen }: Props) { + return ( + + + + + + + + + + + {track.title} + {track.subtitle && <>
{track.subtitle}} +
+
+
+ + + + + + + + +
+
+ ); +} diff --git a/src/components/Shazarr/ui/StatusChip.tsx b/src/components/Shazarr/ui/StatusChip.tsx index 71a6c7a..6e51d4b 100644 --- a/src/components/Shazarr/ui/StatusChip.tsx +++ b/src/components/Shazarr/ui/StatusChip.tsx @@ -6,27 +6,25 @@ import { useConfigProvider } from "../../Config/useConfig"; import { useShazarrProvider } from "../useShazarr"; export default function StatusChip() { - const { shazarrLoading, shazarrResponse, recordingStatus } = + const { shazarrLoading, showInlineResult, recordingStatus } = useShazarrProvider(); const { isNetworkConnected } = useConfigProvider(); return ( - ) : shazarrResponse ? ( + ) : showInlineResult ? ( ) : !isNetworkConnected ? ( @@ -37,13 +35,11 @@ export default function StatusChip() { label={ recordingStatus && recordingStatus !== "inactive" ? `${recordingStatus}...` - : shazarrResponse + : showInlineResult ? "Found !" - : shazarrResponse && !shazarrResponse - ? "Not found. Get closer." - : !isNetworkConnected - ? "Offline mode" - : "Ready" + : !isNetworkConnected + ? "Offline mode" + : "Ready" } variant="outlined" /> diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png index ee42fa9..ccf7723 100644 Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-1-Android-Chrome-linux.png differ diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png index e8693fa..dbff4b3 100644 Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-2-Android-Chrome-linux.png differ diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png index 7d19799..42db13b 100644 Binary files a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-3-Android-Chrome-linux.png differ diff --git a/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png new file mode 100644 index 0000000..42db13b Binary files /dev/null and b/testing/snapshots/critical.spec.ts-snapshots/Critical-Should-be-able-to-record-recognize-and-display-result-4-Android-Chrome-linux.png differ diff --git a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png index eb53cf1..fe301fa 100644 Binary files a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png and b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-1-Android-Chrome-linux.png differ diff --git a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png index 0e525c1..bff6b4f 100644 Binary files a/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png and b/testing/snapshots/history.spec.ts-snapshots/History-Should-be-able-to-consult-2-different-items-2-Android-Chrome-linux.png differ diff --git a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png index 79f4cd1..a326a99 100644 Binary files a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png and b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-1-Android-Chrome-linux.png differ diff --git a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png index 9be4c67..78c55c5 100644 Binary files a/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png and b/testing/snapshots/options.spec.ts-snapshots/Options-Should-see-options-panel-and-use-service-buttons-2-Android-Chrome-linux.png differ diff --git a/testing/tests/critical.spec.ts b/testing/tests/critical.spec.ts index 8e84ac6..f349270 100644 --- a/testing/tests/critical.spec.ts +++ b/testing/tests/critical.spec.ts @@ -9,17 +9,18 @@ test("Critical: Should be able to record, recognize and display result", async ( await expect(page.getByText("Offroad")).toBeVisible(); await expect(page.getByText("Ready")).toBeVisible(); await expect( - page.getByRole("button", { name: "Services configuration" }), + page.getByRole("button", { name: "Configuration" }), ).toBeVisible(); await expect(page).toHaveScreenshot(); // Run song recognition - await page.getByRole("button").first().click(); + await page.getByTestId("record-button").click(); await expect(page.getByText("recording...")).toBeVisible(); await page.waitForTimeout(5000); - // Expect result + // Expect inline result card (not a drawer) await expect(page.getByText("Found !")).toBeVisible(); + await expect(page.getByTestId("inline-result-card")).toBeVisible(); const loader = await page.locator("img.loading")?.isVisible(); if (loader) { await expect(page.locator("img.loading")).not.toBeVisible({ @@ -28,6 +29,29 @@ test("Critical: Should be able to record, recognize and display result", async ( } await page.waitForTimeout(1000); + await expect(page.getByTestId("inline-result-card")).toContainText("Chillin'"); + await expect(page.getByTestId("inline-result-card")).toContainText("Modjo"); + await expect(page).toHaveScreenshot(); + + // Dismiss inline card (close button, NOT delete from history) + await page.getByTestId("inline-result-card").getByRole("button", { name: "Dismiss result" }).click(); + await expect(page.getByTestId("inline-result-card")).not.toBeVisible(); + + // Expect to be on main screen with Records button visible + await expect(page.getByText("Ready")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Records" }), + ).toBeVisible(); + + // Open history drawer + await page.getByRole("button", { name: "Records" }).click(); + await expect(page.getByTestId("history-item")).toBeVisible(); + await expect(page.getByTestId("history-item")).toContainText( + "Chillin' - Modjo", + ); + + // Click history item to open Result drawer + await page.getByTestId("history-item").getByRole("button").first().click(); await expect(page.getByText("Chillin'")).toHaveCount(2); await expect(page.getByText("Modjo", { exact: true })).toBeVisible(); await expect(page).toHaveScreenshot(); @@ -35,28 +59,7 @@ test("Critical: Should be able to record, recognize and display result", async ( await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await expect(page).toHaveScreenshot(); - // Expect history - await page.getByRole("button", { name: "Show records (1)" }).click(); - - await expect(page.getByTestId("history-item")).toBeVisible(); - await expect(page.getByTestId("history-item")).toContainText( - "Chillin' - Modjo", - ); - await expect(page.getByTestId("history-item").locator("button")).toHaveCount( - 4, - ); - - // Expect closing history - await page.getByRole("button", { name: "Close records" }).click(); - await expect(page.getByTestId("history-item")).not.toBeVisible(); - - // Expect closing track result + // Close Result drawer await page.getByRole("button", { name: "Close" }).click(); - await expect(page.getByTestId("Close")).not.toBeVisible(); - - // Expect to be on main screen await expect(page.getByText("Ready")).toBeVisible(); - await expect( - page.getByRole("button", { name: "Show records (1)" }), - ).toBeVisible(); }); diff --git a/testing/tests/history.spec.ts b/testing/tests/history.spec.ts index c7b60c2..8ba9be5 100644 --- a/testing/tests/history.spec.ts +++ b/testing/tests/history.spec.ts @@ -15,7 +15,7 @@ test("History: Should see tracks history", async ({ page }) => { await expect(page.getByText("Ready")).toBeInViewport(); // Open history panel - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await expect(page.getByTestId("history-item")).toHaveCount(3); const rows = page.getByTestId("history-item"); @@ -25,10 +25,7 @@ test("History: Should see tracks history", async ({ page }) => { } // Click on history item - await page - .locator("div:nth-child(3) > .MuiCardActions-root > button") - .first() - .click(); + await page.getByTestId("history-item").nth(2).getByRole("button").first().click(); await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza"); await expect(page.getByText("Spotify")).toBeVisible(); @@ -47,19 +44,16 @@ test("History: Should be able to remove 1 item", async ({ page }) => { await expect(page.getByText("Ready")).toBeInViewport(); // Open history panel - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); // Click on history item - await page - .locator("div:nth-child(2) > .MuiCardActions-root > button") - .last() - .click(); + await page.getByTestId("history-item").nth(1).getByRole("button").last().click(); await expect(page.getByTestId("history-item")).toHaveCount(2); // Refresh and check await page.reload(); - await page.getByRole("button", { name: "Show records (2)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await expect(page.getByText(expectedHistory[0])).toBeVisible(); await expect(page.getByText(expectedHistory[1])).not.toBeVisible(); @@ -73,27 +67,21 @@ test("History: Should be able to remove 2 items", async ({ page }) => { await expect(page.getByText("Ready")).toBeInViewport(); // Open history panel - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); // Click on history item - await page - .locator("div:nth-child(1) > .MuiCardActions-root > button") - .last() - .click(); + await page.getByTestId("history-item").nth(0).getByRole("button").last().click(); await expect(page.getByTestId("history-item")).toHaveCount(2); // Click on history item - await page - .locator("div:nth-child(1) > .MuiCardActions-root > button") - .last() - .click(); + await page.getByTestId("history-item").nth(0).getByRole("button").last().click(); await expect(page.getByTestId("history-item")).toHaveCount(1); // Refresh and check await page.reload(); - await page.getByRole("button", { name: "Show records (1)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await expect(page.getByText(expectedHistory[0])).not.toBeVisible(); await expect(page.getByText(expectedHistory[1])).not.toBeVisible(); @@ -109,26 +97,23 @@ test("History: Should be able to consult 2 different items", async ({ await expect(page.getByText("Ready")).toBeInViewport(); // Open history panel - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); // Click on history item - await page - .locator("div:nth-child(1) > .MuiCardActions-root > button") - .first() - .click(); + await page.getByTestId("history-item").nth(0).getByRole("button").first().click(); await expect( page.getByText("Chillin'ModjoPlay AlbumModjo"), ).toHaveScreenshot({ maxDiffPixelRatio: 0.15 }); + // Close result before reopening history + await page.getByRole("button", { name: "Close" }).click(); + // Open history panel - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); // Click on history item - await page - .locator("div:nth-child(2) > .MuiCardActions-root > button") - .first() - .click(); + await page.getByTestId("history-item").nth(1).getByRole("button").first().click(); await expect( page.getByText( diff --git a/testing/tests/lidarr.spec.ts b/testing/tests/lidarr.spec.ts index 2e67997..8197cc6 100644 --- a/testing/tests/lidarr.spec.ts +++ b/testing/tests/lidarr.spec.ts @@ -24,14 +24,14 @@ async function gotoWithConfig(page: Page, config: Record) { } async function openYakuzaResult(page: Page) { - await page.getByRole("button", { name: "Show records (3)" }).click(); - await page.locator("div:nth-child(3) > .MuiCardActions-root > button").first().click(); + await page.getByRole("button", { name: "Records" }).click(); + await page.getByTestId("history-item").nth(2).getByRole("button").first().click(); await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza"); } async function openModjoResult(page: Page) { - await page.getByRole("button", { name: "Show records (3)" }).click(); - await page.locator("div:nth-child(1) > .MuiCardActions-root > button").first().click(); + await page.getByRole("button", { name: "Records" }).click(); + await page.getByTestId("history-item").nth(0).getByRole("button").first().click(); await expect(page.locator(".MuiTypography-h5")).toHaveText("Chillin'"); } diff --git a/testing/tests/offline.spec.ts b/testing/tests/offline.spec.ts index 976cb4d..f45c148 100644 --- a/testing/tests/offline.spec.ts +++ b/testing/tests/offline.spec.ts @@ -5,20 +5,22 @@ test("Offline: Should save record for further recognition", async ({ }) => { await page.context().setOffline(false); await page.goto("/"); + await page.evaluate(() => window.localStorage.clear()); + await page.reload(); await expect(page.getByText("Shazarr")).toBeVisible(); + await expect(page.getByText("Ready", { exact: true })).toBeVisible(); - // Run song recognition and cut internet connection - await page.getByRole("button").first().click(); - await expect(page.getByText("recording...")).toBeVisible(); await page.context().setOffline(true); - await page.waitForTimeout(5000); - await expect(page.getByText("Offline mode", { exact: true })).toBeVisible(); await expect(page.getByText("Internet is not reachable. In")).toBeVisible(); + // Run song recognition and cut internet connection + await page.getByTestId("record-button").click(); + + // Open history panel - await page.getByRole("button", { name: "Show records (1)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await expect(page.getByTestId("history-item")).toHaveCount(1); await expect( page @@ -32,11 +34,12 @@ test("Offline: Should save record for further recognition", async ({ await page.reload(); // Search - await page.getByRole("button", { name: "Show records (1)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await page.getByTestId("history-item").getByRole("button").nth(1).click(); await expect(page.getByText("searching...")).toBeVisible(); await page.waitForTimeout(5000); await expect(page.getByText("Found !")).toBeVisible(); - await expect(page.getByText("Chillin'")).toHaveCount(2); - await expect(page.getByText("Modjo", { exact: true })).toBeVisible(); + await expect(page.getByTestId("inline-result-card")).toBeVisible(); + await expect(page.getByTestId("inline-result-card")).toContainText("Chillin'"); + await expect(page.getByTestId("inline-result-card")).toContainText("Modjo"); }); diff --git a/testing/tests/options.spec.ts b/testing/tests/options.spec.ts index f1ea9d9..e5d6d12 100644 --- a/testing/tests/options.spec.ts +++ b/testing/tests/options.spec.ts @@ -52,12 +52,9 @@ test("Options: Should see options panel and use service buttons", async ({ ).not.toBeVisible(); // Go to result page - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); - await page - .locator("div:nth-child(3) > .MuiCardActions-root > button") - .first() - .click(); + await page.getByTestId("history-item").nth(2).getByRole("button").first().click(); await waitForImgLoaded(page); await page.waitForTimeout(1000); @@ -86,10 +83,5 @@ test("Options: Should see options panel and use service buttons", async ({ await page.reload(); await page.getByRole("button", { name: "Configuration" }).click(); - await expect( - page - .getByRole("dialog") - .locator("div") - .filter({ hasText: "Github page - v0.0.0Save" }), - ).toHaveScreenshot(); + await expect(page.getByRole("dialog")).toHaveScreenshot(); }); diff --git a/testing/tests/tidarr.spec.ts b/testing/tests/tidarr.spec.ts index 4d5e9d4..e70a6ed 100644 --- a/testing/tests/tidarr.spec.ts +++ b/testing/tests/tidarr.spec.ts @@ -42,7 +42,7 @@ async function gotoWithConfig(page: Page, config: Record) { } async function openYakuzaResult(page: Page) { - await page.getByRole("button", { name: "Show records (3)" }).click(); + await page.getByRole("button", { name: "Records" }).click(); await page.locator("div:nth-child(3) > .MuiCardActions-root > button").first().click(); await expect(page.locator(".MuiTypography-h5")).toHaveText("Yakuza"); }