diff --git a/.github/workflows/github-test.yml b/.github/workflows/github-test.yml new file mode 100644 index 0000000..2da500c --- /dev/null +++ b/.github/workflows/github-test.yml @@ -0,0 +1,24 @@ +name: Testing github workflow + +on: + push: + branches: + - main + +jobs: + a_test_job: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: github context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: commits + env: + COMMITS: ${{ toJson(github.event.commits) }} + run: echo "$COMMITS" + - name: commit messages + env: + COMMIT_MESSAGES: ${{ toJson(github.event.commits.*.message) }} + run: echo "$COMMIT_MESSAGES" \ No newline at end of file diff --git a/.github/workflows/hello.yml b/.github/workflows/hello.yml new file mode 100644 index 0000000..ec57373 --- /dev/null +++ b/.github/workflows/hello.yml @@ -0,0 +1,19 @@ +name: Hello world! + +on: + push: + branches: + - main +jobs: + hello-world-job: + runs-on: ubuntu-latest + steps: + - name: say hello world! + run: | + echo "Hello World!" + - name: print current date + run: date + + + - name: list the directory + run: ls -l diff --git a/.github/workflows/period.yml b/.github/workflows/period.yml new file mode 100644 index 0000000..bdd93e0 --- /dev/null +++ b/.github/workflows/period.yml @@ -0,0 +1,18 @@ +name: Periodic Hello + +on: + workflow_dispatch: + schedule: + - cron: '*/15 * * * *' + +jobs: + health_check: + runs-on: ubuntu-latest + + steps: + - name: Check if the deployed app is healthy + uses: jtalk/url-health-check-action@v5 + with: + url: https://pokemon-ci-cd.onrender.com/health + max-attempts: 3 + retry-delay: 3s \ No newline at end of file diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..03931a9 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,116 @@ +name: Deployment pipeline + +on: + push: + branches: + - main + + pull_request: + branches: [ main ] + types: [ opened, synchronize ] + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: install the dependancies + run: npm install + + - name: Check style + run: npm run eslint + + - name: Run Test + run: npm test + + - name: Build step + run: npm run build + + - name: Install playwright drivers + run: | + npx playwright install + sudo npx playwright install-deps + + - name: end-to-end test + run: npm run test:e2e + + deploy: + needs: build_and_test + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: install the dependancies + run: npm install + - name: Trigger deployment + if: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, '#skip') }} + run: curl ${{ secrets.RENDER_DEPLOY_HOOK }} + + tag_release: + needs: [ build_and_test, deploy ] + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: install the dependancies + run: npm install + + - name: Bump version and push tag + if: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, '#skip') }} + uses: anothrNick/github-tag-action@1.75.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEFAULT_BUMP: minor + + notificatfy_success: + needs: [build_and_test, deploy ] + + runs-on: ubuntu-latest + + steps: + - name: Send notification to discord + uses: sarisia/actions-status-discord@v1 + if: ${{ always() && github.event_name == 'push' && !contains(github.event.head_commit.message, '#skip') }} + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + # content: "Hello <@&${{ secrets.DISCORD_ROLE_ID }}>" + title: "Deployment" + description: | + Build and deploy to Github Pages + image: ${{ secrets.EMBED_IMAGE }} + color: ${{ job.status == 'success' && 'green' || job.status == 'failure' && 'red' || 'yellow' }} + + notify_failure: + needs: [build_and_test,deploy ] + + runs-on: ubuntu-latest + + steps: + - name: Send error notification + uses: sarisia/actions-status-discord@v1 + if: ${{ failure() && github.event_name == 'push' && !contains(github.event.head_commit.message, '#skip') }} + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + content: "Hello <@&${{ secrets.DISCORD_ID }}>" + title: "Deployment" + description: | + Build and deploy to Github Pages + image: ${{ secrets.EMBED_IMAGE }} + color: ${{ job.status == 'success' && 'green' || job.status == 'failure' && 'red' || 'yellow' }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 763301f..66c5631 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/README.md b/README.md index e92ae9c..b0057df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Full Stack open CI/CD -This repository is used for the CI/CD module of the Full Stack Open course +> [!Notice] +> This repository is used for the CI/CD module of the Full Stack Open course ## Commands @@ -11,3 +12,9 @@ Start by running `npm install` inside the project folder `npm run eslint` to run eslint `npm run build` to make a production build `npm run start-prod` to run your production build + +## deployed app + +The app is deployed on render + +visit [here](https://pokemon-ci-cd.onrender.com) diff --git a/app.js b/app.js index a676372..c1f3fc8 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,16 @@ -const express = require("express"); -const app = express(); - +const express = require('express') +const app = express() // get the port from env variable -const PORT = process.env.PORT || 5001; +const PORT = process.env.PORT || 5001 + +app.use(express.static('dist')) +app.get('/health', (req,res) => { + res.status(200).json({ status: 'ok' }) +}) -app.use(express.static("dist")); const start = async () => { - await app.listen(PORT) + app.listen(PORT) console.log(`server started on port ${PORT}`) } diff --git a/e2e-tests/Pokemon.spec.js b/e2e-tests/Pokemon.spec.js new file mode 100644 index 0000000..dc21603 --- /dev/null +++ b/e2e-tests/Pokemon.spec.js @@ -0,0 +1,26 @@ +// @ts-check +import { test, expect } from '@playwright/test' +import { describe } from 'node:test' + +describe('Pokedex', () => { + test('front page can be opened', async ({ page }) => { + await page.goto('http://localhost:8080/') + + await expect(page.getByText('ivysaur')).toBeVisible() + + await expect( + page.getByText( + 'Pokémon and Pokémon character names are trademarks of Nintendo.' + ) + ).toBeVisible() + }) + test('expect a pokemon description to be visible', async ({ page })=> + { + await page.goto('http://localhost:8080/pokemon/ivysaur') + await expect( + page.getByText( + 'chlorophyll' + ) + ).toBeVisible() + }) +}) \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js index d8ec8ac..d67d217 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,5 @@ -const { TextEncoder, TextDecoder } = require('util') +/* eslint-disable no-undef */ +import { TextEncoder, TextDecoder } from 'util' global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder diff --git a/package-lock.json b/package-lock.json index 29fad56..15a412a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,10 @@ "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", + "@playwright/test": "^1.60.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/node": "^25.9.1", "babel-jest": "^30.3.0", "babel-loader": "^10.1.1", "css-loader": "^7.1.4", @@ -3532,6 +3534,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -3874,13 +3892,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/qs": { @@ -11274,6 +11292,53 @@ "node": ">=16.0.0" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13514,9 +13579,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 52679c8..156f1c4 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "start": "webpack-dev-server --open --mode development --port 8080", "start-prod": "node app.js", "test": "jest", + "test:e2e": "npx playwright test", "eslint": "eslint './**/*.{js,jsx}'", "build": "webpack --mode production" }, @@ -32,15 +33,22 @@ }, "jest": { "testEnvironment": "jsdom", - "setupFiles": ["/jest.setup.js"] + "setupFiles": [ + "/jest.setup.js" + ], + "testPathIgnorePatterns": [ + "e2e-tests" + ] }, "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", + "@playwright/test": "^1.60.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/node": "^25.9.1", "babel-jest": "^30.3.0", "babel-loader": "^10.1.1", "css-loader": "^7.1.4", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..6c644b8 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,82 @@ +// @ts-check +/* eslint-disable no-undef */ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './e2e-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* 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:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + }, +}); + diff --git a/src/App.jsx b/src/App.jsx index 6b7e7cb..2660189 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ import React from 'react' import { BrowserRouter as Router, Routes, Route, useMatch } from 'react-router-dom' import { useApi } from './useApi' diff --git a/src/PokemonPage.jsx b/src/PokemonPage.jsx index a37849f..d4ebaf1 100644 --- a/src/PokemonPage.jsx +++ b/src/PokemonPage.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import React from 'react' import { Link, useParams } from 'react-router-dom' import LoadingSpinner from './LoadingSpinner' @@ -26,13 +27,13 @@ const PokemonPage = ({ previous, next }) => { const normalAbility = pokemon.abilities.find((ability) => !ability.is_hidden) const hiddenAbility = pokemon.abilities.find((ability) => ability.is_hidden === true) - console.log('hiddenAbility=', hiddenAbility) + console.log('hiddenAbility=', normalAbility) return ( <>
{previous && Previous} Home - {next && Next} + {next && Next}