test
This commit is contained in:
commit
1b61871097
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=""
|
20
.eslintrc.cjs
Normal file
20
.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
ecosystem.config.js
|
||||||
|
deploy.key
|
||||||
|
|
||||||
|
coverage
|
||||||
|
coverage.txt
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
3
.lintstagedrc.json
Normal file
3
.lintstagedrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"*.{js,jsx}": "prettier --write"
|
||||||
|
}
|
3244
ProjetBut3.postman_collection.json
Normal file
3244
ProjetBut3.postman_collection.json
Normal file
File diff suppressed because it is too large
Load Diff
32
README.md
Normal file
32
README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
# Projet BUT 3
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
Clone the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone gitea@dwarves.iut-fbleau.fr:legrelle/Projet-dev-but3-2024.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the project directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Projet-dev-but3-2024
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the front-end
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
API Documentation : [Postman]([Postman documentation url])
|
19
__tests__/App.test.jsx
Normal file
19
__tests__/App.test.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// src/__ tests __/App.test.tsx
|
||||||
|
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import App from "../src/App.jsx";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
test("demo", () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Renders the main page", () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>,
|
||||||
|
);
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
51
__tests__/pages/authenticated/Login.test.jsx
Normal file
51
__tests__/pages/authenticated/Login.test.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
||||||
|
import { render, fireEvent, screen, cleanup } from "@testing-library/react";
|
||||||
|
import { Login } from "../../../src/pages/index.js";
|
||||||
|
import { AuthenticationContext } from "../../../src/contexts/index.js";
|
||||||
|
|
||||||
|
describe("Login Component", () => {
|
||||||
|
let loginFunction;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loginFunction = vi.fn(); // Mock login function
|
||||||
|
render(
|
||||||
|
<AuthenticationContext.Provider value={{ login: loginFunction }}>
|
||||||
|
<Login />
|
||||||
|
</AuthenticationContext.Provider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without errors", () => {
|
||||||
|
expect(screen.getByText("Login page")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows entering a username and password", () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("username"), {
|
||||||
|
target: { value: "testuser" },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("password"), {
|
||||||
|
target: { value: "password123" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText("username").value).toBe("testuser");
|
||||||
|
expect(screen.getByPlaceholderText("password").value).toBe("password123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles submit event", async () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("username"), {
|
||||||
|
target: { value: "testuser" },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("password"), {
|
||||||
|
target: { value: "password123" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("submit"));
|
||||||
|
|
||||||
|
expect(loginFunction).toHaveBeenCalledWith("testuser", "password123");
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO add more tests please :)
|
||||||
|
});
|
9
e2e-tests/example.spec.js
Normal file
9
e2e-tests/example.spec.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("has title on my project", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle("Vite + React");
|
||||||
|
});
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5454
package-lock.json
generated
Normal file
5454
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "projet-but3",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"test": "vitest",
|
||||||
|
"test-e2e": "npx playwright test",
|
||||||
|
"show-e2e": "npx playwright show-report",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write 'src/**/*.{js,jsx}'",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-cookie": "^7.0.2",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.21.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.42.1",
|
||||||
|
"@testing-library/react": "^14.1.2",
|
||||||
|
"@types/node": "^20.11.9",
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"happy-dom": "^13.3.1",
|
||||||
|
"husky": "^9.0.6",
|
||||||
|
"lint-staged": "^15.2.0",
|
||||||
|
"prettier": "^3.2.4",
|
||||||
|
"vite": "^5.0.8",
|
||||||
|
"vitest": "^1.2.2"
|
||||||
|
},
|
||||||
|
"description": "Clone the project",
|
||||||
|
"main": "playwright.config.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
79
playwright.config.js
Normal file
79
playwright.config.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { defineConfig, devices } = require('@playwright/test');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './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://127.0.0.1:3000',
|
||||||
|
|
||||||
|
/* 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://127.0.0.1:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
13
src/App.jsx
Normal file
13
src/App.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Authenticated } from "./components";
|
||||||
|
|
||||||
|
import { Router } from "./router";
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<Authenticated>
|
||||||
|
<Router />
|
||||||
|
</Authenticated>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
30
src/api/authentication.js
Normal file
30
src/api/authentication.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const isLoggedIn = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/authenticate");
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = async (username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/authenticate", { username, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete("/authenticate");
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
};
|
2
src/api/index.js
Normal file
2
src/api/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './authentication'
|
||||||
|
export * from './user'
|
14
src/api/user.js
Normal file
14
src/api/user.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const createUser = async (username, password, confirmation) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/user", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
confirmation,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
};
|
0
src/assets/styles/index.css
Normal file
0
src/assets/styles/index.css
Normal file
7
src/components/authenticated.jsx
Normal file
7
src/components/authenticated.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { AuthenticationProvider } from "../contexts";
|
||||||
|
|
||||||
|
export const Authenticated = ({ children }) => {
|
||||||
|
return <AuthenticationProvider>{children}</AuthenticationProvider>;
|
||||||
|
};
|
1
src/components/index.js
Normal file
1
src/components/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./authenticated";
|
98
src/contexts/auth-context.jsx
Normal file
98
src/contexts/auth-context.jsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createUser as registerApi,
|
||||||
|
isLoggedIn,
|
||||||
|
login as loginApi,
|
||||||
|
logout as logoutApi,
|
||||||
|
} from "../api";
|
||||||
|
import { useQuery } from "../hooks";
|
||||||
|
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const initState = {
|
||||||
|
user: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthenticationContext = React.createContext({
|
||||||
|
...initState,
|
||||||
|
login: () => {},
|
||||||
|
register: () => {},
|
||||||
|
logout: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthenticationProvider = ({ children }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const query = useQuery();
|
||||||
|
const [authState, setAuthState] = useState(initState);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const redirect = () => {
|
||||||
|
navigate(query.get("redirect_uri") || "/");
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (email, password) => {
|
||||||
|
loginApi(email, password)
|
||||||
|
.then((user) => {
|
||||||
|
setAuthState({ user });
|
||||||
|
redirect();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAuthState({ user: undefined });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (username, password, confirmation) => {
|
||||||
|
registerApi(username, password, confirmation).then((user) => {
|
||||||
|
setAuthState({ user });
|
||||||
|
redirect();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
logoutApi().then(() => {
|
||||||
|
setAuthState(initState);
|
||||||
|
navigate(`/login`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isLoggedIn()
|
||||||
|
.then((user) => {
|
||||||
|
if (user === "Unauthorized") throw new Error("Unauthorized");
|
||||||
|
setAuthState({ user });
|
||||||
|
if (location.pathname === "/login") {
|
||||||
|
redirect();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAuthState({ user: undefined });
|
||||||
|
|
||||||
|
if (!location.pathname.match(/^(\/|\/login|\/register)$/)) {
|
||||||
|
navigate(`/login?redirect_uri=${encodeURI(location.pathname)}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname === "/logout") {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationContext.Provider
|
||||||
|
value={{ ...authState, login, logout, register }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthenticationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
1
src/contexts/index.js
Normal file
1
src/contexts/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './auth-context'
|
2
src/hooks/index.js
Normal file
2
src/hooks/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-auth";
|
||||||
|
export * from "./use-query";
|
7
src/hooks/use-auth.js
Normal file
7
src/hooks/use-auth.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { AuthenticationContext } from "../contexts";
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
return React.useContext(AuthenticationContext);
|
||||||
|
}
|
9
src/hooks/use-query.js
Normal file
9
src/hooks/use-query.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
export const useQuery = () => {
|
||||||
|
const { search } = useLocation()
|
||||||
|
|
||||||
|
return React.useMemo(() => new URLSearchParams(search), [search])
|
||||||
|
}
|
19
src/main.jsx
Normal file
19
src/main.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./app.jsx";
|
||||||
|
import "./assets/styles/index.css";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
import setupAxios from "./setupAxios";
|
||||||
|
|
||||||
|
setupAxios();
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<CookiesProvider>
|
||||||
|
<App />
|
||||||
|
</CookiesProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
2
src/pages/authenticated/index.js
Normal file
2
src/pages/authenticated/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './login'
|
||||||
|
export * from './register'
|
42
src/pages/authenticated/login.jsx
Normal file
42
src/pages/authenticated/login.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useAuth } from "../../hooks";
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const onSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const response = await login(username, password);
|
||||||
|
|
||||||
|
if (response && !response.success) {
|
||||||
|
setError(response.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Login page</h1>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
placeholder="username"
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit">submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
49
src/pages/authenticated/register.jsx
Normal file
49
src/pages/authenticated/register.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useAuth } from "../../hooks";
|
||||||
|
|
||||||
|
export const Register = () => {
|
||||||
|
const { register } = useAuth();
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmation, setConfirmation] = useState("");
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const onSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const response = await register(username, password, confirmation);
|
||||||
|
|
||||||
|
if (response && !response.success) {
|
||||||
|
setError(response.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Register page</h1>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
placeholder="username"
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmation}
|
||||||
|
placeholder="confirmation"
|
||||||
|
onChange={(e) => setConfirmation(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit">submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
14
src/pages/home.jsx
Normal file
14
src/pages/home.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useAuth } from "../hooks";
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Home page</h1>
|
||||||
|
{user && <h2>Hello {user.user.username}</h2>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
2
src/pages/index.js
Normal file
2
src/pages/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./authenticated";
|
||||||
|
export * from "./home";
|
12
src/router.jsx
Normal file
12
src/router.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Home, Login, Register } from "./pages";
|
||||||
|
|
||||||
|
export const Router = () => (
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path="login" element={<Login />} />
|
||||||
|
<Route path="register" element={<Register />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
8
src/setupAxios.js
Normal file
8
src/setupAxios.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const setupAxios = () => {
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setupAxios;
|
449
tests-examples/demo-todo-app.spec.js
Normal file
449
tests-examples/demo-todo-app.spec.js
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('https://demo.playwright.dev/todomvc');
|
||||||
|
});
|
||||||
|
|
||||||
|
const TODO_ITEMS = [
|
||||||
|
'buy some cheese',
|
||||||
|
'feed the cat',
|
||||||
|
'book a doctors appointment'
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe('New Todo', () => {
|
||||||
|
test('should allow me to add todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create 1st todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list only has one todo item.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2nd todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list now has two todo items.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[1]
|
||||||
|
]);
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear text input field when an item is added', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create one todo item.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Check that input is empty.
|
||||||
|
await expect(newTodo).toBeEmpty();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||||
|
// Create 3 items.
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
// Check test using different methods.
|
||||||
|
await expect(page.getByText('3 items left')).toBeVisible();
|
||||||
|
await expect(todoCount).toHaveText('3 items left');
|
||||||
|
await expect(todoCount).toContainText('3');
|
||||||
|
await expect(todoCount).toHaveText(/3/);
|
||||||
|
|
||||||
|
// Check all items in one call.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Mark all as completed', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||||
|
// Complete all todos.
|
||||||
|
await page.getByLabel('Mark all as complete').check();
|
||||||
|
|
||||||
|
// Ensure all todos have 'completed' class.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
// Check and then immediately uncheck.
|
||||||
|
await toggleAll.check();
|
||||||
|
await toggleAll.uncheck();
|
||||||
|
|
||||||
|
// Should be no completed classes.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
await toggleAll.check();
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Uncheck first todo.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').uncheck();
|
||||||
|
|
||||||
|
// Reuse toggleAll locator and make sure its not checked.
|
||||||
|
await expect(toggleAll).not.toBeChecked();
|
||||||
|
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Assert the toggle all is checked again.
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Item', () => {
|
||||||
|
|
||||||
|
test('should allow me to mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check first item.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
|
||||||
|
// Check second item.
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await secondTodo.getByRole('checkbox').check();
|
||||||
|
|
||||||
|
// Assert completed class.
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).toHaveClass('completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||||
|
|
||||||
|
await firstTodoCheckbox.check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await firstTodoCheckbox.uncheck();
|
||||||
|
await expect(firstTodo).not.toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to edit an item', async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const secondTodo = todoItems.nth(1);
|
||||||
|
await secondTodo.dblclick();
|
||||||
|
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
// Explicitly assert the new text value.
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2]
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Editing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide other controls when editing', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||||
|
await todoItem.dblclick();
|
||||||
|
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||||
|
await expect(todoItem.locator('label', {
|
||||||
|
hasText: TODO_ITEMS[1],
|
||||||
|
})).not.toBeVisible();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should save edits on blur', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should trim entered text', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should cancel edits on escape', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||||
|
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Counter', () => {
|
||||||
|
test('should display the current number of todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
await expect(todoCount).toContainText('1');
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
await expect(todoCount).toContainText('2');
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Clear completed button', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display the correct text', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove completed items when clicked', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).getByRole('checkbox').check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(todoItems).toHaveCount(2);
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Persistence', () => {
|
||||||
|
test('should persist its data', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||||
|
await firstTodoCheck.check();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
|
||||||
|
// Ensure there is 1 completed item.
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
// Now reload.
|
||||||
|
await page.reload();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Routing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
// make sure the app had a chance to save updated todos in storage
|
||||||
|
// before navigating to a new view, otherwise the items can get lost :(
|
||||||
|
// in some frameworks like Durandal
|
||||||
|
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display active items', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect the back button', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await test.step('Showing all items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing active items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing completed items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(todoItem).toHaveCount(1);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display completed items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display all items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should highlight the currently applied filter', async ({ page }) => {
|
||||||
|
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||||
|
|
||||||
|
//create locators for active and completed links
|
||||||
|
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||||
|
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||||
|
await activeLink.click();
|
||||||
|
|
||||||
|
// Page change - active items.
|
||||||
|
await expect(activeLink).toHaveClass('selected');
|
||||||
|
await completedLink.click();
|
||||||
|
|
||||||
|
// Page change - completed items.
|
||||||
|
await expect(completedLink).toHaveClass('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createDefaultTodos(page) {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} expected
|
||||||
|
*/
|
||||||
|
async function checkNumberOfTodosInLocalStorage(page, expected) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} expected
|
||||||
|
*/
|
||||||
|
async function checkNumberOfCompletedTodosInLocalStorage(page, expected) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} title
|
||||||
|
*/
|
||||||
|
async function checkTodosInLocalStorage(page, title) {
|
||||||
|
return await page.waitForFunction(t => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t);
|
||||||
|
}, title);
|
||||||
|
}
|
19
tests/example.spec.js
Normal file
19
tests/example.spec.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Playwright/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get started link', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Click the get started link.
|
||||||
|
await page.getByRole('link', { name: 'Get started' }).click();
|
||||||
|
|
||||||
|
// Expects page to have a heading with the name of Installation.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||||
|
});
|
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
// Use happy-dom for a more lightweight browser environment
|
||||||
|
environment: "happy-dom",
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
14
vitest.config.js
Normal file
14
vitest.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ["__tests__/**/*.test.{js,jsx}"],
|
||||||
|
environment: "happy-dom",
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user