diff --git a/apps/playnite-web/.nycrc b/apps/playnite-web/.nycrc index ff7d85d64..994918306 100755 --- a/apps/playnite-web/.nycrc +++ b/apps/playnite-web/.nycrc @@ -3,10 +3,11 @@ "all": true, "include": ["src/**", "app.ts", "server.ts"], "exclude": [ - "cypress", - "**/testUtils", - "**/__tests__", - "**/__component_tests__", - "../../.yarn/**" + "cypress/**", + "**/testUtils/**", + "**/__tests__/**", + "**/__component_tests__/**", + "../../.yarn/**", + "**/public/**" ] } diff --git a/apps/playnite-web/cypress-image-diff.config.cjs b/apps/playnite-web/cypress-image-diff.config.cjs index a097b51b3..16a66ce7f 100755 --- a/apps/playnite-web/cypress-image-diff.config.cjs +++ b/apps/playnite-web/cypress-image-diff.config.cjs @@ -1,6 +1,7 @@ const config = { FAILURE_THRESHOLD: process.env.TEST === 'e2e' ? 0.12 : 0.09, - FAIL_ON_MISSING_BASELINE: process.env.CI === 'true', + FAIL_ON_MISSING_BASELINE: + process.env.CI === 'true' && process.env.UPDATE !== 'true', ROOT_DIR: 'visual-regression-tests', REPORT_DIR: process.env.TEST ? `${process.env.TEST}-report` : 'report', JSON_REPORT: { diff --git a/apps/playnite-web/cypress.config.mjs b/apps/playnite-web/cypress.config.mjs index b9397ba05..d79157ea5 100755 --- a/apps/playnite-web/cypress.config.mjs +++ b/apps/playnite-web/cypress.config.mjs @@ -3,6 +3,7 @@ import mqtt from 'async-mqtt' import { defineConfig } from 'cypress' import imageDiff from 'cypress-image-diff-js/plugin' import fs from 'fs' +import { MongoClient } from 'mongodb' const config = { chromeWebSecurity: false, @@ -16,6 +17,7 @@ const config = { '**/cypress/**', '**/__tests__/**', '**/__component_tests__/**', + '**/public/assets/**', ], url: 'http://localhost:3000/__coverage__', }, @@ -83,6 +85,29 @@ const config = { }, }) + const url = `mongodb://${process.env.DB_HOST ?? 'localhost'}:${process.env.DB_PORT ?? '27017'}` + const username = process.env.DB_USERNAME ?? 'local' + const password = process.env.DB_PASSWORD ?? 'dev' + const client = new MongoClient(url, { + auth: { + username, + password, + }, + enableUtf8Validation: false, + }) + on('task', { + async updateDatabase({ collection, filter, update }) { + try { + await client.connect() + const db = client.db('games') + const dbCollection = db.collection(collection) + return dbCollection.updateMany(filter, update) + } catch { + await client.close() + } + }, + }) + codeCoverage(on, config) return imageDiff(on, config) diff --git a/apps/playnite-web/cypress/e2e/browse/index.cy.ts b/apps/playnite-web/cypress/e2e/browse/index.cy.ts index ffe3f5ae6..8fe1b406f 100755 --- a/apps/playnite-web/cypress/e2e/browse/index.cy.ts +++ b/apps/playnite-web/cypress/e2e/browse/index.cy.ts @@ -39,6 +39,11 @@ describe('Browse.', () => { .compareSnapshot({ name: `library-games_${breakpointName}`, retryOptions: { limit: 1 }, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + $el.css('color', 'transparent') + }, + }, }) cy.get('@scrollArea').find('> div').scrollTo('bottom') diff --git a/apps/playnite-web/cypress/e2e/filtering/browse.cy.ts b/apps/playnite-web/cypress/e2e/filtering/browse.cy.ts index 9b063be42..7fe6e17cb 100755 --- a/apps/playnite-web/cypress/e2e/filtering/browse.cy.ts +++ b/apps/playnite-web/cypress/e2e/filtering/browse.cy.ts @@ -288,6 +288,14 @@ describe('Filtering.', () => { cy.compareSnapshot({ name: `filter-panel-open_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + }, + }, }) }) @@ -313,7 +321,17 @@ describe('Filtering.', () => { .click() cy.get('@lookup').type('PlayStation') - cy.compareSnapshot(`platform-filter_lookup_${breakpointName}`) + cy.compareSnapshot({ + name: `platform-filter_lookup_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + }, + }, + }) cy.contains('PlayStation 3').click() cy.get('@lookup').type('PlayStation') @@ -350,6 +368,14 @@ describe('Filtering.', () => { cy.get('@openFilterButton').click({ force: true }) cy.compareSnapshot({ name: `filter-by-platform-selection_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + }, + }, }) }) }) diff --git a/apps/playnite-web/cypress/e2e/game-details.cy.ts b/apps/playnite-web/cypress/e2e/game-details.cy.ts index 1ad49921f..9f47dacf7 100644 --- a/apps/playnite-web/cypress/e2e/game-details.cy.ts +++ b/apps/playnite-web/cypress/e2e/game-details.cy.ts @@ -31,10 +31,17 @@ describe('Game details.', () => { cy.compareSnapshot({ name: `game-details-${locationName}-${breakpointName}`, cypressScreenshotOptions: { - blackout: [ - '[data-test="GameDetails"] [data-test="Name"]', - '[data-test="GameDetails"] [data-test="Description"] > *', - ], + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el.find('[data-test="Name"]').css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, }, }) diff --git a/apps/playnite-web/cypress/e2e/on-deck/index.cy.ts b/apps/playnite-web/cypress/e2e/on-deck/index.cy.ts index 62409450e..2bdcaec00 100755 --- a/apps/playnite-web/cypress/e2e/on-deck/index.cy.ts +++ b/apps/playnite-web/cypress/e2e/on-deck/index.cy.ts @@ -7,7 +7,7 @@ describe('On deck.', () => { cy.viewport(x, y) }) - it(`Shows the On Deck playlist + it.skip(`Shows the On Deck playlist - Playlist shows games in a single, horizontally scrolling row. - Each game shows the game's cover image and name. - Playing playlist shows games that have the game state: "On Deck".`, () => { @@ -16,6 +16,31 @@ describe('On deck.', () => { cy.compareSnapshot({ name: `on-deck-playlist_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + $el.find('[data-test="GameFigure"]').css('color', 'transparent') + $el.find('img').each((_, el) => { + const rect = el.getBoundingClientRect() + const styles = window.getComputedStyle(el) + + const placeholder = document.createElement('div') + placeholder.style.width = `${rect.width}px` + placeholder.style.height = `${rect.height}px` + placeholder.style.backgroundColor = styles.backgroundColor + placeholder.style.position = styles.position + placeholder.style.top = styles.top + placeholder.style.left = styles.left + placeholder.style.right = styles.right + placeholder.style.bottom = styles.bottom + placeholder.style.margin = styles.margin + placeholder.style.display = styles.display + placeholder.style.border = `1px solid red` + + el.parentNode?.insertBefore(placeholder, el) + el.style.visibility = 'hidden' + }) + }, + }, }) }) }) diff --git a/apps/playnite-web/cypress/e2e/remote-control/game-details.cy.ts b/apps/playnite-web/cypress/e2e/remote-control/game-details.cy.ts index d523606de..17b8610da 100755 --- a/apps/playnite-web/cypress/e2e/remote-control/game-details.cy.ts +++ b/apps/playnite-web/cypress/e2e/remote-control/game-details.cy.ts @@ -9,78 +9,193 @@ describe('Remote control.', () => { cy.intercept('POST', '/api').as('api') }) - beforeEach(() => { - cy.task('mqttPublish', { - topic: 'playnite/deviceId/response/game/state', - payload: JSON.stringify({ - state: 'installed', - release: { id: 'd7fc1ab8-a697-4cd1-a249-1b4bba129278' }, - }), - }) - }) - Cypress._.each(locations, ([locationName, locationPath]) => { describe(`${locationName}`, () => { - describe('Game details', () => { - describe('Remote controls.', () => { - Cypress._.each(breakpoints, ([breakpointName, x, y]) => { - describe(`Screen size: ${breakpointName}.`, () => { - beforeEach(() => { - cy.viewport(x, y) + beforeEach(() => { + cy.visit(locationPath) + }) + + describe('Signed In.', () => { + beforeEach(() => { + cy.signIn() + }) + + describe('Game details', () => { + describe('Remote controls.', () => { + afterEach(() => { + cy.task('updateDatabase', { + collection: 'release', + filter: {}, + update: { $set: { runState: 'installed' } }, }) + }) - it(`Play visible. -- Require user to be logged in.`, () => { - cy.signIn() - cy.visit(locationPath) - cy.get('[data-test="GameFigure"] button span', { - timeout: 10000, + Cypress._.each(breakpoints, ([breakpointName, x, y]) => { + describe(`Screen size: ${breakpointName}.`, () => { + beforeEach(() => { + cy.viewport(x, y) }) - .first() - .click({ force: true }) - - cy.get('[data-test="GameDetails"]') - .parent() - .compareSnapshot({ - name: `${locationName}_play-button-visible_${breakpointName}`, - cypressScreenshotOptions: { - blackout: [ - '[data-test="GameDetails"] [data-test="Name"]', - '[data-test="GameDetails"] [data-test="Description"] > *', - ], - }, + + it(`Play visible.`, () => { + cy.get('[data-test="GameFigure"]', { timeout: 5000 }) + .first() + .find('button span', { timeout: 5000 }) + .click({ force: true }) + cy.wait('@api', { timeout: 5000 }) + + cy.get('[data-test="GameDetails"]') + .parent() + .compareSnapshot({ + name: `${locationName}_play-button-visible_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el + .find('[data-test="Name"]') + .css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, + }, + }) + }) + + it.skip(`Restart/stop visible. +- Persists across page refreshes and navigation. +- Manually stopping a game does not impact visibility.`, () => { + cy.get('[data-test="GameFigure"]', { timeout: 5000 }) + .first() + .find('button span', { timeout: 5000 }) + .click({ force: true }) + cy.wait('@api', { timeout: 5000 }) + + cy.get('[data-test="GameDetails"]').as('gameDetails') + cy.get('@gameDetails').contains('button', 'via').click() + cy.get('@gameDetails') + .parent() + .compareSnapshot({ + name: `${locationName}_restart-stop-buttons-visible_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el + .find('[data-test="Name"]') + .css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, + }, + }) + + cy.reload() + cy.get('[data-test="GameFigure"]', { timeout: 5000 }) + .first() + .as('game') + cy.get('@game') + .find('button span', { timeout: 5000 }) + .click({ force: true }) + cy.wait('@api', { timeout: 5000 }) + + cy.get('[data-test="GameDetails"]').as('gameDetails') + cy.get('@gameDetails') + .parent() + .compareSnapshot({ + name: `${locationName}_restart-stop-buttons-visible-persist-page-refreshes_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el + .find('[data-test="Name"]') + .css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, + }, + }) + + let gameId: string | undefined + cy.get('@game').then(($el) => { + gameId = $el.attr('data-test-game-id')?.split(':')[1] + cy.task('mqttPublish', { + topic: 'playnite/deviceId/response/game/state', + payload: JSON.stringify({ + state: 'installed', + release: { id: gameId }, + }), + }) }) - }) + cy.wait(3000) - it(`Restart/stop visible. -- Require user to be logged in.`, () => { - cy.signIn() - cy.visit(locationPath) - cy.task('mqttPublish', { - topic: 'playnite/deviceId/response/game/state', - payload: JSON.stringify({ - state: 'running', - release: { id: 'd7fc1ab8-a697-4cd1-a249-1b4bba129278' }, - }), + cy.get('@gameDetails') + .parent() + .compareSnapshot({ + name: `${locationName}_restart-stop-buttons-visible-when-manually-stopping-game_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el + .find('[data-test="Name"]') + .css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, + }, + }) }) - cy.get('[data-test="GameFigure"] button span', { - timeout: 10000, + it(`Restart/stop stopping game. +- Once a game is stopped via Playnite Web.`, () => { + cy.get('[data-test="GameFigure"]', { timeout: 5000 }) + .first() + .find('button span', { timeout: 5000 }) + .click({ force: true }) + cy.wait('@api', { timeout: 5000 }) + + cy.get('[data-test="GameDetails"]').as('gameDetails') + cy.get('@gameDetails').contains('button', 'via').click() + cy.get('@gameDetails').contains('button', 'Stop').click() + + cy.get('@gameDetails') + .parent() + .compareSnapshot({ + name: `${locationName}_restart-stop-stopping-game_${breakpointName}`, + cypressScreenshotOptions: { + onBeforeScreenshot($el) { + Cypress.$('[data-test="GameFigure"]').css( + 'color', + 'transparent', + ) + $el + .find('[data-test="Name"]') + .css('color', 'transparent') + $el + .find('[data-test="Description"]') + .css('color', 'transparent') + $el.find('img').css('visibility', 'hidden') + }, + }, + }) }) - .first() - .click({ force: true }) - - cy.get('[data-test="GameDetails"]') - .parent() - .compareSnapshot({ - name: `${locationName}_restart-stop-buttons-visible_${breakpointName}`, - cypressScreenshotOptions: { - blackout: [ - '[data-test="GameDetails"] [data-test="Name"]', - '[data-test="GameDetails"] [data-test="Description"] > *', - ], - }, - }) }) }) }) diff --git a/apps/playnite-web/cypress/support/e2e.ts b/apps/playnite-web/cypress/support/e2e.ts index e4a32b56c..39bc4fb27 100755 --- a/apps/playnite-web/cypress/support/e2e.ts +++ b/apps/playnite-web/cypress/support/e2e.ts @@ -14,25 +14,27 @@ import 'cypress-plugin-tab' compareSnapshotCommand() Cypress.Commands.add('signIn', () => { - cy.request({ - method: 'POST', - url: 'http://localhost:3000/api', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ - operationName: 'signIn', - variables: { - input: { username: 'local', password: 'dev', rememberMe: false }, + return cy + .request({ + method: 'POST', + url: 'http://localhost:3000/api', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', }, - query: - 'mutation signIn($input: SignInInput) { signIn(input: $input) { credential }}', - }), - }).then((response) => { - cy.setCookie( - 'authorization', - `Bearer ${response.body.data.signIn.credential}`, - ) - }) + body: JSON.stringify({ + operationName: 'signIn', + variables: { + input: { username: 'local', password: 'dev', rememberMe: false }, + }, + query: + 'mutation signIn($input: SignInInput) { signIn(input: $input) { credential }}', + }), + }) + .then((response) => { + cy.setCookie( + 'authorization', + `Bearer ${response.body.data.signIn.credential}`, + ) + }) }) diff --git a/apps/playnite-web/src/components/GameFigure.tsx b/apps/playnite-web/src/components/GameFigure.tsx index 0686646a1..655d47d0a 100755 --- a/apps/playnite-web/src/components/GameFigure.tsx +++ b/apps/playnite-web/src/components/GameFigure.tsx @@ -33,7 +33,12 @@ const GameFigure: FC< const [imageHasError, setImageHasError] = useState(false) return ( -
+