Setup Playwright (#14)

* setting up playwright

* renaming client to app

* updating node version

* updating node version

* fixing husky

* fixing husky

* fixing husky

* fixing husky
This commit was merged in pull request #14.
This commit is contained in:
2024-05-22 21:12:17 -05:00
committed by GitHub
parent 977db84d48
commit 294ac4e061
40 changed files with 3018 additions and 367 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.vscode/* .vscode/*
node_modules node_modules
**/.env

View File

@@ -1 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged npx lint-staged

View File

@@ -4,7 +4,7 @@ module.exports = {
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended'
], ],
ignorePatterns: ['dist', '.eslintrc.cjs'], ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
@@ -12,7 +12,7 @@ module.exports = {
rules: { rules: {
'react-refresh/only-export-components': [ 'react-refresh/only-export-components': [
'warn', 'warn',
{ allowConstantExport: true }, { allowConstantExport: true }
], ]
}, }
}; };

View File

View File

@@ -20,8 +20,8 @@ export default {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'], project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname
}, }
}; };
``` ```

View File

@@ -27,5 +27,8 @@
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0"
},
"engines": {
"node": ">=18.0.0"
} }
} }

View File

@@ -1,6 +1,6 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
}, }
}; };

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -4,16 +4,15 @@ import Logbook from './components/logbook/Logbook';
import Checklists from './components/checklists/Checklists'; import Checklists from './components/checklists/Checklists';
import Pilots from './components/pilots/Pilots'; import Pilots from './components/pilots/Pilots';
const App: React.FC<unknown> = () => { const App: React.FC<unknown> = () => {
return ( return (
<Routes> <Routes>
<Route path='/' element={<Flights />} /> <Route path="/" element={<Flights />} />
<Route path='logbook' element={<Logbook />} /> <Route path="logbook" element={<Logbook />} />
<Route path='checklists' element={<Checklists />} /> <Route path="checklists" element={<Checklists />} />
<Route path='pilots' element={<Pilots />} /> <Route path="pilots" element={<Pilots />} />
</Routes> </Routes>
); );
} };
export default App; export default App;

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,12 @@
import SiteNav from '../siteNav/SiteNav';
const Logbook: React.FC<unknown> = () => {
return (
<div>
<SiteNav />
<div>Logbook goes here</div>
</div>
);
};
export default Logbook;

View File

@@ -0,0 +1,122 @@
import React from 'react';
import {
Accordion,
AccordionItem,
Button,
DatePicker,
Input
} from '@nextui-org/react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
const LogbookEntryForm: React.FC<unknown> = () => {
const { control, handleSubmit } = useForm();
const onSubmit = (data: unknown) => {
console.log(data);
};
return (
<form className="m-10" onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-2 gap-4">
<div className="self-center">
<label>Date</label>
</div>
<div>
<Controller
name="date"
control={control}
render={({ field }) => <DatePicker labelPlacement="outside-left" />}
/>
</div>
<div className="self-center">
<label>Aircraft Type</label>
</div>
<div>
<Controller
name="aircraftType"
control={control}
render={({ field }) => (
<Input fullWidth={true} labelPlacement="outside-left" />
)}
/>
</div>
<div className="self-center">
<label>Aircraft Identity</label>
</div>
<div>
<Controller
name="aircraftIdent"
control={control}
render={({ field }) => (
<Input fullWidth={true} labelPlacement="outside-left" />
)}
/>
</div>
<div className="self-center">
<label>Route To</label>
</div>
<div>
<Controller
name="routeTo"
control={control}
render={({ field }) => <Input />}
/>
</div>
<div className="self-center">
<label>Route From</label>
</div>
<div>
<Controller
name="routeFrom"
control={control}
render={({ field }) => <Input />}
/>
</div>
<div className="self-center">
<label>Duration of Flight</label>
</div>
<div>
<Controller
name="flightDuration"
control={control}
render={({ field }) => <Input />}
/>
</div>
<div className="self-center">
<label>Single Engine Land</label>
</div>
<div>
<Controller
name="aircraftSEL"
control={control}
render={({ field }) => <Input />}
/>
</div>
<div className="col-span-2">
<Accordion>
<AccordionItem title="Aircraft Category and Class">
<div className="self-center">
<label>Single Engine Land</label>
</div>
<div>
<Controller
name="aircraftSEL"
control={control}
render={({ field }) => <Input />}
/>
</div>
</AccordionItem>
</Accordion>
</div>
<div className="col-span-2 justify-self-end">
<Button color="default">Cancel</Button>
<Button className="ml-10" color="primary">
Save
</Button>
</div>
</div>
</form>
);
};
export default LogbookEntryForm;

View File

@@ -0,0 +1,85 @@
import {
Accordion,
AccordionItem,
Button,
DatePicker,
Input,
Select,
SelectItem
} from '@nextui-org/react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
const PilotForm: React.FC<unknown> = () => {
const { control, handleSubmit } = useForm();
const onSubmit = (data: unknown) => {
console.log(data);
};
return (
<form className="m-10" onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-2 gap-4">
<div className="self-center">
<label>First Name</label>
</div>
<div>
<Controller
name="firstName"
control={control}
render={({ field }) => <Input />}
/>
</div>
<div className="self-center">
<label>Last Name</label>
</div>
<div>
<Controller
name="lastName"
control={control}
render={({ field }) => <Input />}
/>
</div>
</div>
<Accordion>
<AccordionItem title="Medical Certificate">
<div className="grid grid-cols-2 gap-4">
<div className="self-center">
<label>Class</label>
</div>
<div>
<Controller
name="medicalClass"
control={control}
render={({ field }) => (
<Select>
<SelectItem key="first" value="First">
First
</SelectItem>
<SelectItem key="second" value="Second">
Second
</SelectItem>
<SelectItem key="Third" value="Third">
Third
</SelectItem>
</Select>
)}
/>
</div>
<div className="self-center">
<label>Expires</label>
</div>
<div>
<Controller
name="medicalExpiration"
control={control}
render={({ field }) => <DatePicker />}
/>
</div>
</div>
</AccordionItem>
</Accordion>
</form>
);
};
export default PilotForm;

View File

@@ -0,0 +1,15 @@
import SiteNav from '../siteNav/SiteNav';
import PilotForm from '../pilotForm/PilotForm';
const Pilots: React.FC<unknown> = () => {
return (
<div>
<SiteNav />
<div>
<PilotForm />
</div>
</div>
);
};
export default Pilots;

View File

@@ -0,0 +1,59 @@
import {
Avatar,
Link,
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Button
} from '@nextui-org/react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
const SiteNav: React.FC<unknown> = () => {
const isAuthenticated = useIsAuthenticated();
const { accounts, instance } = useMsal();
const initializeLogin = () => {
instance.loginRedirect();
};
console.log(accounts);
return (
<Navbar isBordered>
<NavbarBrand>Site Logo Goes Here</NavbarBrand>
<NavbarContent justify="center">
<NavbarItem>
<Link>
<ReactRouterLink to="/">Flights</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to="/logbook">Logbook</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to="/checklists">Checklists</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to="/pilots">Pilots</ReactRouterLink>
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
{!isAuthenticated && (
<Button as={Link} color="primary" href="#" onClick={initializeLogin}>
Login
</Button>
)}
{isAuthenticated && <Avatar name={accounts[0]?.username} />}
</NavbarContent>
</Navbar>
);
};
export default SiteNav;

View File

@@ -10,10 +10,11 @@ import { BrowserRouter } from 'react-router-dom';
const configuration: Configuration = { const configuration: Configuration = {
auth: { auth: {
clientId: 'd3562a45-050d-4f9a-baed-0497c7156924', clientId: 'd3562a45-050d-4f9a-baed-0497c7156924',
authority: 'https://login.microsoftonline.com/0f23652e-4b15-420f-991e-3d6fc769a31d', authority:
'https://login.microsoftonline.com/0f23652e-4b15-420f-991e-3d6fc769a31d',
redirectUri: 'http://localhost:5173' redirectUri: 'http://localhost:5173'
} }
} };
const pca = new PublicClientApplication(configuration); const pca = new PublicClientApplication(configuration);
@@ -26,5 +27,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
</BrowserRouter> </BrowserRouter>
</NextUIProvider> </NextUIProvider>
</MsalProvider> </MsalProvider>
</React.StrictMode>, </React.StrictMode>
); );

View File

@@ -5,11 +5,11 @@ export default {
content: [ content: [
'./index.html', './index.html',
'./src/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
], ],
theme: { theme: {
extend: {}, extend: {}
}, },
darkMode: 'class', darkMode: 'class',
plugins: [nextui()], plugins: [nextui()]
}; };

View File

@@ -3,5 +3,5 @@ import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()]
}); });

View File

@@ -1,14 +0,0 @@
import SiteNav from '../siteNav/SiteNav';
const Logbook: React.FC<unknown> = () => {
return (
<div>
<SiteNav />
<div>
Logbook goes here
</div>
</div>
)
}
export default Logbook;

View File

@@ -1,138 +0,0 @@
import React from 'react';
import { Accordion, AccordionItem, Button, DatePicker, Input } from '@nextui-org/react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
const LogbookEntryForm: React.FC<unknown> = () => {
const { control, handleSubmit } = useForm();
const onSubmit = (data: unknown) => {
console.log(data)
}
return (
<form className='m-10' onSubmit={handleSubmit(onSubmit)}>
<div className='grid grid-cols-2 gap-4'>
<div className='self-center'>
<label>Date</label>
</div>
<div>
<Controller
name='date'
control={control}
render={({ field }) => (
<DatePicker
labelPlacement='outside-left'
/>
)}
/>
</div>
<div className='self-center'>
<label>Aircraft Type</label>
</div>
<div>
<Controller
name='aircraftType'
control={control}
render={({ field }) => (
<Input
fullWidth={true}
labelPlacement='outside-left'
/>
)}
/>
</div>
<div className='self-center'>
<label>Aircraft Identity</label>
</div>
<div>
<Controller
name='aircraftIdent'
control={control}
render={({ field }) => (
<Input
fullWidth={true}
labelPlacement='outside-left'
/>
)}
/>
</div>
<div className='self-center'>
<label>Route To</label>
</div>
<div>
<Controller
name='routeTo'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
<div className='self-center'>
<label>Route From</label>
</div>
<div>
<Controller
name='routeFrom'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
<div className='self-center'>
<label>Duration of Flight</label>
</div>
<div>
<Controller
name='flightDuration'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
<div className='self-center'>
<label>Single Engine Land</label>
</div>
<div>
<Controller
name='aircraftSEL'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
<div className='col-span-2'>
<Accordion>
<AccordionItem title='Aircraft Category and Class'>
<div className='self-center'>
<label>Single Engine Land</label>
</div>
<div>
<Controller
name='aircraftSEL'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
</AccordionItem>
</Accordion>
</div>
<div className='col-span-2 justify-self-end'>
<Button color='default'>
Cancel
</Button>
<Button className='ml-10' color='primary'>
Save
</Button>
</div>
</div>
</form>
)
}
export default LogbookEntryForm;

View File

@@ -1,83 +0,0 @@
import { Accordion, AccordionItem, Button, DatePicker, Input, Select, SelectItem } from '@nextui-org/react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
const PilotForm: React.FC<unknown> = () => {
const { control, handleSubmit } = useForm();
const onSubmit = (data: unknown) => {
console.log(data);
}
return (
<form className='m-10' onSubmit={handleSubmit(onSubmit)}>
<div className='grid grid-cols-2 gap-4'>
<div className='self-center'>
<label>First Name</label>
</div>
<div>
<Controller
name='firstName'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
<div className='self-center'>
<label>Last Name</label>
</div>
<div>
<Controller
name='lastName'
control={control}
render={({ field }) => (
<Input />
)}
/>
</div>
</div>
<Accordion>
<AccordionItem title='Medical Certificate'>
<div className='grid grid-cols-2 gap-4'>
<div className='self-center'>
<label>Class</label>
</div>
<div>
<Controller
name='medicalClass'
control={control}
render={({ field }) => (
<Select>
<SelectItem key='first' value='First'>
First
</SelectItem>
<SelectItem key='second' value='Second'>
Second
</SelectItem>
<SelectItem key='Third' value='Third'>
Third
</SelectItem>
</Select>
)}
/>
</div>
<div className='self-center'>
<label>Expires</label>
</div>
<div>
<Controller
name='medicalExpiration'
control={control}
render={({ field }) => (
<DatePicker />
)}
/>
</div>
</div>
</AccordionItem>
</Accordion>
</form>
)
}
export default PilotForm;

View File

@@ -1,15 +0,0 @@
import SiteNav from '../siteNav/SiteNav';
import PilotForm from '../pilotForm/PilotForm';
const Pilots: React.FC<unknown> = () => {
return (
<div>
<SiteNav />
<div>
<PilotForm />
</div>
</div>
)
}
export default Pilots;

View File

@@ -1,53 +0,0 @@
import { Avatar, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, Button } from '@nextui-org/react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
const SiteNav: React.FC<unknown> = () => {
const isAuthenticated = useIsAuthenticated();
const { accounts, instance } = useMsal();
const initializeLogin = () => {
instance.loginRedirect();
}
console.log(accounts)
return (
<Navbar isBordered>
<NavbarBrand>
Site Logo Goes Here
</NavbarBrand>
<NavbarContent justify='center'>
<NavbarItem>
<Link>
<ReactRouterLink to='/'>Flights</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to='/logbook'>Logbook</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to='/checklists'>Checklists</ReactRouterLink>
</Link>
</NavbarItem>
<NavbarItem>
<Link>
<ReactRouterLink to='/pilots'>Pilots</ReactRouterLink>
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify='end'>
{!isAuthenticated &&
<Button as={Link} color='primary' href='#' onClick={initializeLogin}>Login</Button>
}
{isAuthenticated &&
<Avatar name={accounts[0]?.username} />
}
</NavbarContent>
</Navbar>
)
}
export default SiteNav;

845
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@
"version": "1.0.0", "version": "1.0.0",
"workspaces": [ "workspaces": [
"api", "api",
"client" "app",
"tests"
], ],
"scripts": { "scripts": {
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"", "format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
"lint": "npm run lint -w api && npm run lint -w client", "lint": "npm run lint -w api && npm run lint -w app",
"prepare": "husky" "prepare": "husky || true"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",

0
tests/cucumber.mjs Normal file
View File

View File

@@ -0,0 +1,30 @@
import { LaunchOptions, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const browserOptions: LaunchOptions = {
headless: Boolean(process.env.HEADLESS) || false,
timeout: Number(process.env.TIMEOUT) || 60000,
args: [
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream'
],
firefoxUserPrefs: {
'media.navigator.streams.fake': true,
'media.navigator.permission.disable': true
},
slowMo: Number(process.env.SLO_MO) || 0
};
const desktopChrome = devices['Desktop Chrome'];
export const config = {
browser: process.env.BROWSER || 'chromium',
browserOptions,
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
username: process.env.TEST_USERNAME,
password: process.env.TEST_PASSWORD,
viewportHeight: process.env.VIEWPORT_HEIGHT || desktopChrome.viewport.height,
viewportWidth: process.env.VIEWPORT_WIDTH || desktopChrome.viewport.width,
userAgent: process.env.USER_AGENT || desktopChrome.userAgent
};

121
tests/e2e/support/hooks.ts Normal file
View File

@@ -0,0 +1,121 @@
import { ICustomWorld } from './world';
import { config } from './config';
import {
After,
AfterAll,
Before,
BeforeAll,
Status,
setDefaultTimeout
} from '@cucumber/cucumber';
import {
chromium,
ChromiumBrowser,
firefox,
FirefoxBrowser,
webkit,
WebKitBrowser,
ConsoleMessage,
ViewportSize
} from '@playwright/test';
import { ITestCaseHookParameter } from '@cucumber/cucumber/lib/support_code_library_builder/types';
import { ensureDir } from 'fs-extra';
const tracesDir = './e2e/results/traces';
let browser: ChromiumBrowser | FirefoxBrowser | WebKitBrowser;
declare global {
let browser: ChromiumBrowser | FirefoxBrowser | WebKitBrowser;
}
setDefaultTimeout(process.env.PWDEBUG ? -1 : 60 * 100);
BeforeAll(async function () {
switch (config.browser) {
case 'firefox': {
browser = await firefox.launch(config.browserOptions);
break;
}
case 'webkit': {
browser = await webkit.launch(config.browserOptions);
break;
}
default: {
browser = await chromium.launch(config.browserOptions);
}
}
await ensureDir(tracesDir);
});
Before({ tags: '@ignore' }, async function () {
return 'skipped' as any;
});
Before({ tags: '@debug' }, async function (this: ICustomWorld) {
this.debug = true;
});
Before(async function (this: ICustomWorld, { pickle }: ITestCaseHookParameter) {
const viewportSize: ViewportSize = {
height: Number(1080),
width: Number(1920)
};
this.startTime = new Date();
this.testName = pickle.name.replace(/\W/g, '-');
this.context = await browser.newContext({
acceptDownloads: true,
viewport: viewportSize,
userAgent: config.userAgent
});
await this.context.tracing.start({
screenshots: true,
snapshots: true
});
this.page = await this.context.newPage();
this.page.on('console', async (msg: ConsoleMessage) => {
if (msg.type() === 'log') {
await this.attach(msg.text());
}
});
this.feature = pickle;
});
After(async function (this: ICustomWorld, { result }: ITestCaseHookParameter) {
if (result) {
await this.attach(
`Status: ${result?.status}. Duration:${result.duration?.seconds}s`
);
if (result.status !== Status.PASSED) {
const image = await this.page?.screenshot({
path: `./e2e/results/screenshots/${this.testName}.png`,
type: 'png'
});
const timePart = this.startTime
?.toISOString()
.split('.')[0]
.replace(/:/g, '_');
if (image) {
await this.attach(image, 'image/png');
}
await this.context?.tracing.stop({
path: `${tracesDir}/${this.testName}-${timePart}trace.zip`
});
}
}
await this.page?.close();
await this.context?.close();
});
AfterAll(async function () {
await browser.close();
});

View File

View File

@@ -0,0 +1,33 @@
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber';
import * as messages from '@cucumber/messages';
import {
BrowserContext,
Page,
PlaywrightTestOptions,
APIRequestContext
} from '@playwright/test';
export interface CucumberWorldConstructorParams {
parameters: { [key: string]: string };
}
export interface ICustomWorld extends World {
debug: boolean;
feature?: messages.Pickle;
context?: BrowserContext;
page?: Page;
testName?: string;
startTime?: Date;
server?: APIRequestContext;
playwrightOptions?: PlaywrightTestOptions;
}
export class CustomWorld extends World implements ICustomWorld {
constructor(options: IWorldOptions) {
super(options);
}
debug = false;
}
setWorldConstructor(CustomWorld);

1665
tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
tests/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@cucumber/cucumber": "^10.7.0",
"@playwright/test": "^1.44.0",
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0"
}
}

20
tests/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"typeRoots": ["../node_modules/@types"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}