diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d3538e6..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -cypress diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js new file mode 100644 index 0000000..37c79ff --- /dev/null +++ b/cypress/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.json", + }, +}; diff --git a/cypress/consts/urlRegexes.js b/cypress/consts/urlRegexes.ts similarity index 100% rename from cypress/consts/urlRegexes.js rename to cypress/consts/urlRegexes.ts diff --git a/cypress/integration/about/about.spec.ts b/cypress/integration/about/about.spec.ts new file mode 100644 index 0000000..d77206b --- /dev/null +++ b/cypress/integration/about/about.spec.ts @@ -0,0 +1,11 @@ +describe("About", () => { + it("should not have broken links", () => { + cy.visit(`${Cypress.env("hostUrl")}/sobre`); + + cy.getByDataTest("accordion-container").within(() => { + cy.findAllByRole("link").each((link) => { + cy.request(link.prop("href")).its("status").should("eq", 200); + }); + }); + }); +}); diff --git a/cypress/integration/header/header.spec.js b/cypress/integration/header/header.spec.js deleted file mode 100644 index e99ae33..0000000 --- a/cypress/integration/header/header.spec.js +++ /dev/null @@ -1,22 +0,0 @@ -import { streamsRegex, tagsRegex, vodsRegex } from "../../consts/urlRegexes"; - -describe("Home > Filter", () => { - it("should have correct links", () => { - const LINKS = [ - { text: "Assistir", href: "/" }, - { text: "Sobre", href: "/sobre" }, - { text: "Estatísticas", href: "/estatisticas" }, - { text: "Agradecimentos", href: "/agradecimentos" }, - { text: "GitHub", href: "https://github.com/brdevstreamers" }, - { text: "Discord", href: "https://discord.gg/collabcode" }, - ]; - - cy.visit(Cypress.env("hostUrl")); - - cy.getByData("header-link").should("have.length", LINKS.length); - - LINKS.forEach(({ text, href }) => { - cy.getByData("header-link").contains(text).should("have.attr", "href", href); - }); - }); -}); diff --git a/cypress/integration/header/header.spec.ts b/cypress/integration/header/header.spec.ts new file mode 100644 index 0000000..8893c4c --- /dev/null +++ b/cypress/integration/header/header.spec.ts @@ -0,0 +1,11 @@ +describe("Header", () => { + it("should not have broken links", () => { + cy.visit(Cypress.env("hostUrl")); + + cy.getByDataTest("header-container").within(() => { + cy.findAllByRole("link").each((link) => { + cy.request(link.prop("href")).its("status").should("eq", 200); + }); + }); + }); +}); diff --git a/cypress/integration/home/filter.spec.js b/cypress/integration/home/filter.spec.js deleted file mode 100644 index 60e26b0..0000000 --- a/cypress/integration/home/filter.spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { streamsRegex, tagsRegex, vodsRegex } from "../../consts/urlRegexes"; - -describe("Home > Filter", () => { - beforeEach(() => { - cy.intercept(streamsRegex, { - statusCode: 200, - fixture: "streams", - }).as("streams"); - cy.intercept(tagsRegex, { - statusCode: 200, - fixture: "tags", - }).as("tags"); - cy.intercept(vodsRegex, { - statusCode: 200, - fixture: "vods", - }).as("vods"); - }); - - it("should filter out streams by tags change the URL", () => { - const PROGRAMMING_TEXT = "Programação"; - const PROGRAMMING_TEXT_ENCODED = encodeURIComponent(PROGRAMMING_TEXT); - const JAVASCRIPT_TEXT = "JavaScript"; - const LGBTQIA_TEXT = "LGBTQIA+"; - const LGBTQIA_TEXT_ENCODED = encodeURIComponent("LGBTQIA+"); - - cy.visit(Cypress.env("hostUrl")); - cy.wait(["@streams", "@tags", "@vods"]); - - cy.getByData("card-online").should("have.length", 13); - - cy.getByData("tag-filter-item-unselected") - .contains(PROGRAMMING_TEXT) - .should("have.prop", "tagName", "A") - .should("have.attr", "href", `?tags=${PROGRAMMING_TEXT_ENCODED}`) - .click(); - cy.getByData("card-online").should("have.length", 2); - - cy.url().should("contain", `?tags=${PROGRAMMING_TEXT_ENCODED}`); - - cy.getByData("tag-filter-item-selected").contains(PROGRAMMING_TEXT).click(); - cy.getByData("card-online").should("have.length", 13); - - cy.getByData("tag-filter-item-unselected").contains(PROGRAMMING_TEXT).click(); - - cy.getByData("tag-filter-item-unselected") - .contains(JAVASCRIPT_TEXT) - .should("have.prop", "tagName", "A") - .should("have.attr", "href", `?tags=${JAVASCRIPT_TEXT}`) - .click(); - - cy.url().should( - "contain", - `?tags=${encodeURIComponent(`${PROGRAMMING_TEXT},${JAVASCRIPT_TEXT}`)}`, - ); - - cy.getByData("card-online").should("have.length", 1); - - cy.getByData("tag-filter-item-unselected") - .contains(LGBTQIA_TEXT) - .should("have.prop", "tagName", "A") - .should("have.attr", "href", `?tags=${LGBTQIA_TEXT_ENCODED}`) - .click(); - - cy.url().should( - "contain", - `?tags=${encodeURIComponent(`${PROGRAMMING_TEXT},${JAVASCRIPT_TEXT},${LGBTQIA_TEXT}`)}`, - ); - - cy.getByData("card-online").should("have.length", 0); - - cy.getByData("tag-filter-item-selected").contains(LGBTQIA_TEXT).click(); - cy.getByData("card-online").should("have.length", 1); - - cy.getByData("tag-filter-item-selected").contains(JAVASCRIPT_TEXT).click(); - cy.getByData("card-online").should("have.length", 2); - - cy.getByData("tag-filter-item-selected").contains(PROGRAMMING_TEXT).click(); - cy.getByData("card-online").should("have.length", 13); - }); - - it("should accept the tag query string to filter out streams", () => { - const JAVASCRIPT_TEXT = "JavaScript"; - - cy.visit(`http://localhost:3000/?tags=${JAVASCRIPT_TEXT}`); - cy.wait(["@streams", "@tags", "@vods"]); - - cy.getByData("tag-filter-item-selected") - .contains(JAVASCRIPT_TEXT) - .should("have.css", "backgroundColor", "rgb(139, 61, 255)") - .should("have.length", 1); - cy.getByData("card-online").should("have.length", 3); - }); -}); diff --git a/cypress/integration/home/filter.spec.ts b/cypress/integration/home/filter.spec.ts new file mode 100644 index 0000000..ba1f21c --- /dev/null +++ b/cypress/integration/home/filter.spec.ts @@ -0,0 +1,68 @@ +import { streamsRegex, tagsRegex, vodsRegex } from "../../consts/urlRegexes"; + +describe("Home > Filter", () => { + beforeEach(() => { + cy.intercept(streamsRegex, { + statusCode: 200, + fixture: "streams", + }).as("streams"); + cy.intercept(tagsRegex, { + statusCode: 200, + fixture: "tags", + }).as("tags"); + cy.intercept(vodsRegex, { + statusCode: 200, + fixture: "vods", + }).as("vods"); + }); + + it("should filter out streams by tags change the URL", () => { + const PROGRAMMING_TEXT = "Programação"; + const JAVASCRIPT_TEXT = "JavaScript"; + const LGBTQIA_TEXT = "LGBTQIA+"; + + cy.visit(Cypress.env("hostUrl")); + cy.wait(["@streams", "@tags", "@vods"]); + + cy.getByDataTest("card-online").should("have.length", 13); + + cy.filterByTag(PROGRAMMING_TEXT); + cy.shouldUrlContainTags(PROGRAMMING_TEXT); + cy.getByDataTest("card-online").should("have.length", 2); + + cy.unFilterByTag(PROGRAMMING_TEXT); + cy.getByDataTest("card-online").should("have.length", 13); + + cy.filterByTag(PROGRAMMING_TEXT); + cy.filterByTag(JAVASCRIPT_TEXT); + cy.shouldUrlContainTags(PROGRAMMING_TEXT, JAVASCRIPT_TEXT); + cy.getByDataTest("card-online").should("have.length", 1); + + cy.filterByTag(LGBTQIA_TEXT); + cy.shouldUrlContainTags(PROGRAMMING_TEXT, JAVASCRIPT_TEXT, LGBTQIA_TEXT); + cy.getByDataTest("card-online").should("have.length", 0); + + cy.unFilterByTag(LGBTQIA_TEXT); + cy.getByDataTest("card-online").should("have.length", 1); + + cy.unFilterByTag(JAVASCRIPT_TEXT); + cy.getByDataTest("card-online").should("have.length", 2); + + cy.unFilterByTag(PROGRAMMING_TEXT); + cy.getByDataTest("card-online").should("have.length", 13); + }); + + it("should accept the tag query string to filter out streams", () => { + const JAVASCRIPT_TEXT = "JavaScript"; + + cy.visit(`http://localhost:3000/?tags=${JAVASCRIPT_TEXT}`); + cy.wait(["@streams", "@tags", "@vods"]); + + cy.getByDataTest("tag-filter") + .contains(JAVASCRIPT_TEXT) + .should("have.css", "backgroundColor", "rgb(139, 61, 255)") + .should("have.length", 1); + + cy.getByDataTest("card-online").should("have.length", 3); + }); +}); diff --git a/cypress/integration/home/raid.spec.js b/cypress/integration/home/raid.spec.ts similarity index 77% rename from cypress/integration/home/raid.spec.js rename to cypress/integration/home/raid.spec.ts index 113a746..a3acf76 100644 --- a/cypress/integration/home/raid.spec.js +++ b/cypress/integration/home/raid.spec.ts @@ -1,7 +1,7 @@ import { streamsRegex, tagsRegex, vodsRegex } from "../../consts/urlRegexes"; describe("Home > Raid", () => { - it("Copy raid command to clipboard", () => { + it("should copy raid command to clipboard", () => { const COPIED_TEXT = "/raid emersongarrido"; cy.intercept(streamsRegex, { @@ -41,11 +41,13 @@ describe("Home > Raid", () => { cy.wait(["@streams", "@tags", "@vods"]); - cy.getByData("raid-button").first().click(); - cy.window().its("navigator.clipboard").invoke("readText").should("equal", COPIED_TEXT); + cy.getByDataTest("card-online") + .first() + .within(() => { + cy.findByRole("button", { name: "Raid" }).click(); + cy.window().its("navigator.clipboard").invoke("readText").should("equal", COPIED_TEXT); + }); - cy.get(".chakra-alert").within(() => { - cy.get(".chakra-alert__title").contains(`Comando "${COPIED_TEXT}" copiado!`); - }); + cy.findByRole("alert", { name: `Comando "${COPIED_TEXT}" copiado!` }).should("have.length", 1); }); }); diff --git a/cypress/integration/home/refetch.spec.js b/cypress/integration/home/refetch.spec.ts similarity index 85% rename from cypress/integration/home/refetch.spec.js rename to cypress/integration/home/refetch.spec.ts index 019ca8c..a110493 100644 --- a/cypress/integration/home/refetch.spec.js +++ b/cypress/integration/home/refetch.spec.ts @@ -18,7 +18,7 @@ describe("Home > Refetch", () => { cy.visit(Cypress.env("hostUrl")); cy.wait(["@streams", "@tags", "@vods"]); - cy.getByData("card-online").should("have.length", 13); + cy.getByDataTest("card-online").should("have.length", 13); cy.intercept(streamsRegex, { statusCode: 200, @@ -27,6 +27,6 @@ describe("Home > Refetch", () => { cy.tick(120 * 1000) cy.wait(["@streams", "@tags", "@vods"]); - cy.getByData("card-online").should("have.length", 2); + cy.getByDataTest("card-online").should("have.length", 2); }); -}); \ No newline at end of file +}); diff --git a/cypress/integration/home/simultaneous.spec.js b/cypress/integration/home/simultaneous.spec.ts similarity index 69% rename from cypress/integration/home/simultaneous.spec.js rename to cypress/integration/home/simultaneous.spec.ts index a369449..b2f0b7c 100644 --- a/cypress/integration/home/simultaneous.spec.js +++ b/cypress/integration/home/simultaneous.spec.ts @@ -17,10 +17,12 @@ describe("Home > Simultaneous", () => { cy.visit(Cypress.env("hostUrl")); cy.wait(["@streams", "@tags", "@vods"]); - cy.getByData("simultaneous-button").click(); - cy.getByData("start-simultaneous-button").click(); - cy.get(".chakra-toast") - .should("have.length", 1) - .contains("Você deve selecionar pelo menos duas streams"); + cy.getByDataTest("simultaneous-button").click(); + cy.getByDataTest("start-simultaneous-button").click(); + + cy.findByRole("alert", { name: "Você deve selecionar pelo menos duas streams" }).should( + "have.length", + 1, + ); }); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index 59b2bab..0000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -// eslint-disable-next-line no-unused-vars -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts new file mode 100644 index 0000000..94c9ceb --- /dev/null +++ b/cypress/plugins/index.ts @@ -0,0 +1,10 @@ +module.exports = (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { + on("task", { + log(message) { + console.log(message); + return null; + }, + }); + + return config; +}; diff --git a/cypress/support/@types/selectors.d.ts b/cypress/support/@types/selectors.d.ts new file mode 100644 index 0000000..f159545 --- /dev/null +++ b/cypress/support/@types/selectors.d.ts @@ -0,0 +1,7 @@ +export type Selectors = +| "accordion-container" +| "card-online" +| "header-container" +| "simultaneous-button" +| "start-simultaneous-button" +| "tag-filter" diff --git a/cypress/support/commands.js b/cypress/support/commands.js deleted file mode 100644 index 0f3e8e7..0000000 --- a/cypress/support/commands.js +++ /dev/null @@ -1,31 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) - -Cypress.on("uncaught:exception", () => false); - -Cypress.Commands.add("getByData", (selector, ...args) => - cy.get(`[data-test=${selector}]`, ...args), -); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000..c6405b7 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,132 @@ +import type { Selectors } from "./@types/selectors"; + +declare global { + namespace Cypress { + interface Chainable { + /** + * Custom command to get element by data-test attribute + * @param selector data-test attribute value + * @param options + * @example + * cy.getByDataTest('someDataTestValue') + */ + getByDataTest: typeof getByDataTest; + + /** + * Custom command to filter by tag name + * @param tag tag name to filter + * @example + * cy.filterByTag('JavaScript') + */ + filterByTag: typeof filterByTag; + + /** + * Custom command to unfilter by tag name + * @param tag tag name to unfilter + * @example + * cy.unFilterByTag('JavaScript') + */ + unFilterByTag: typeof unFilterByTag; + + /** + * Custom command to check if url contain specified tags + * @param tags tags to check in url + * @example + * cy.shouldUrlContainTags('JavaScript') + * cy.shouldUrlContainTags('JavaScript', 'LGBTQIA+') + */ + shouldUrlContainTags: typeof shouldUrlContainTags; + } + } +} + +const getByDataTest = ( + selector: Selectors, + options?: Partial, +) => { + const log = Cypress.log({ + displayName: "getByDataTest", + name: "Get by [data-test] attribute", + }); + + cy.on("fail", (error) => { + log.error(error); + log.end(); + throw error; + }); + + return cy.get(`[data-test=${selector}]`, options); +}; + +const filterByTag = (tag: string) => { + const encodedTag = encodeURIComponent(tag); + + const log = Cypress.log({ + autoEnd: false, + displayName: "filterByTag", + name: `Filtering by ${tag}`, + }); + + cy.on("fail", (error) => { + log.error(error); + log.end(); + throw error; + }); + + return cy + .getByDataTest("tag-filter") + .contains(tag) + .then(($el) => { + log.set({ $el }); + log.snapshot(); + log.end(); + }) + .should("have.prop", "tagName", "A") + .should("have.attr", "href", `?tags=${encodedTag}`) + .click(); +}; + +const unFilterByTag = (tag: string) => { + const log = Cypress.log({ + autoEnd: false, + displayName: "unFilterByTag", + name: `Unfiltering by ${tag}`, + }); + + cy.on("fail", (error) => { + log.error(error); + log.end(); + throw error; + }); + + return cy + .getByDataTest("tag-filter") + .contains(tag) + .then(($el) => { + log.set({ $el }); + log.snapshot(); + log.end(); + }) + .click(); +}; + +const shouldUrlContainTags = (...tags: Array) => { + Cypress.log({ + displayName: "shouldUrlContainTags", + name: `Check if Url contains ${tags.join(", ")}`, + }); + + cy.url().should("contain", `?tags=${encodeURIComponent(tags.join(","))}`); +}; + +Cypress.on("uncaught:exception", () => false); + +Cypress.Commands.add("getByDataTest", getByDataTest); +Cypress.Commands.add("filterByTag", filterByTag); +Cypress.Commands.add("unFilterByTag", unFilterByTag); +Cypress.Commands.add("shouldUrlContainTags", shouldUrlContainTags); + +/* +eslint + @typescript-eslint/no-namespace: "off", +*/ diff --git a/cypress/support/index.js b/cypress/support/index.js deleted file mode 100644 index d076cec..0000000 --- a/cypress/support/index.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import "./commands"; - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 0000000..4feab96 --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,2 @@ +import "@testing-library/cypress/add-commands"; +import "./commands"; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000..fdc7cb2 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,17 @@ +{ + "exclude": ["../node_modules/@types/jest", "../node_modules/@testing-library/jest-dom"], + "include": ["integration/**/*", "plugins/**/*", "support/**/*"], + "compilerOptions": { + "baseUrl": ".", + "noEmit": true, + "types": ["node", "cypress", "@testing-library/cypress"], + "esModuleInterop": true, + "jsx": "react", + "moduleResolution": "node", + "target": "es2019", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "typeRoots": ["../node_modules/@types"] + } +} diff --git a/package.json b/package.json index c19e178..f9ba83f 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "devDependencies": { "@commitlint/cli": "^16.2.1", "@commitlint/config-conventional": "^16.2.1", + "@testing-library/cypress": "^8.0.2", "@types/cypress": "^1.1.3", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", @@ -87,7 +88,6 @@ "lint-staged": ">=12", "prettier": "^2.5.1" }, - "lint-staged": { "src/**/*": [ "yarn lint --fix" diff --git a/scripts/getSelectors.sh b/scripts/getSelectors.sh new file mode 100755 index 0000000..5fdbb8f --- /dev/null +++ b/scripts/getSelectors.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Credits: https://github.com/filiphric/trelloapp-vue-vite-ts/blob/main/scripts/getSelectors.sh + +# find all data-test attribute in src | select values | +SRC_SELECTORS=$(grep -hro 'data-test="[^"]*"' src | cut -d \" -f2 | sort | uniq) +TEST_SELECTORS=$(grep -hro '.getByDataTest([^"]*' cypress | cut -d \' -f2 | sort | uniq) + +UNUSED_SELECTORS=$(comm -23 <(echo "$SRC_SELECTORS") <(echo "$TEST_SELECTORS")) + +if [[ $UNUSED_SELECTORS != "" ]]; then +echo -e "WARNING! There are some selectors in your app that are not being used: + + +$UNUSED_SELECTORS" +fi + +echo $SRC_SELECTORS | sed "s/ /\"\n| \"/g; s/^/&export type Selectors = \n| \"/; s/.$/&\"/;" | cat > cypress/support/@types/selectors.d.ts diff --git a/src/components/sections/Header.tsx b/src/components/sections/Header.tsx index fd6ce22..f622291 100644 --- a/src/components/sections/Header.tsx +++ b/src/components/sections/Header.tsx @@ -52,7 +52,13 @@ export default function Header() { return ( <> - + Br Dev Streamers @@ -77,7 +83,6 @@ export default function Header() { color: "primary.500", }} _hover={{ textDecoration: "underline" }} - data-test="header-link" > {link.label} @@ -90,7 +95,6 @@ export default function Header() { href={"https://github.com/brdevstreamers"} color={"gray.100"} _hover={{ textDecoration: "underline" }} - data-test="header-link" > GitHub @@ -99,7 +103,6 @@ export default function Header() { href={"https://discord.gg/collabcode"} color={"gray.100"} _hover={{ textDecoration: "underline" }} - data-test="header-link" > Discord diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 104b926..51904fa 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -168,7 +168,6 @@ export default function Card({ {isLive && (