adding pilot

This commit is contained in:
2024-09-19 21:39:23 -05:00
parent ae8e407bce
commit 4600c5e698
15 changed files with 405 additions and 248 deletions

View File

@@ -1,4 +1,11 @@
import { Controller, Get, Headers, Query } from '@nestjs/common';
import {
Controller,
Get,
Headers,
Query,
Res,
StreamableFile
} from '@nestjs/common';
import {
AppConfigService,
MsGraphService,
@@ -8,6 +15,10 @@ import { FeatureFlagValue } from '@azure/app-configuration';
import { Public } from '@noahspan/noahspan-modules';
import { Person } from '@microsoft/microsoft-graph-types';
import { AppService } from './app.service';
import { createReadStream } from 'fs';
import { join } from 'path';
import { arrayBuffer } from 'stream/consumers';
import type { Response } from 'express';
@Controller()
export class AppController {
@@ -23,7 +34,6 @@ export class AppController {
@Query() query: any
): Promise<{ key: string; enabled: boolean }[]> {
try {
console.log(query);
const featureFlagKeys: string[] =
query.keys && query.keys.toString().includes(';')
? query.keys.split(';')
@@ -41,9 +51,27 @@ export class AppController {
}
}
@Public()
@Get('profilePhoto')
async getProfilePhoto(@Query() query: any): Promise<any> {}
@Get('userPhoto')
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
try {
const graphToken: string = await this.msGraphService.getMsGraphAuth(
headers.authorization.replace('Bearer ', ''),
['user.read']
);
const client: MsGraphClient =
await this.msGraphService.getMsGraphClientDelegated(graphToken);
const blob: Blob = await client.api(`me/photos('48x48')/$value`).get();
const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
const buffer: Buffer = Buffer.from(arrayBuffer);
return new StreamableFile(buffer, {
type: 'application/json',
disposition: `attachment; filename="user_photo.png"`
});
} catch (error) {
return error;
}
}
@Get('userProfile')
async getUserProfile(@Headers() headers: any) {

View File

@@ -1,7 +1,6 @@
export class PilotInfoDto {
id: string;
firstName: string;
lastName: string;
name: string;
address: string;
city: string;
state: string;

View File

@@ -2,12 +2,11 @@ export class PilotInfoEntity {
partitionKey: string;
rowKey: string;
id: string;
firstName: string;
lastName: string;
address: string;
city: string;
state: string;
postalCode: string;
name: string;
address?: string;
city?: string;
state?: string;
postalCode?: string;
email?: string;
phone?: string;
}

View File

@@ -1,28 +1,51 @@
import { HttpException, Injectable } from '@nestjs/common';
// import { Repository, InjectRepository } from '@nestjs/azure-database';
import { Injectable } from '@nestjs/common';
import { PilotInfoDto } from './pilot-info.dto';
import { PilotInfoEntity } from './pilot-info.entity';
import { TableClient, TableService } from '@noahspan/noahspan-modules';
import { RestError, TableInsertEntityHeaders } from '@azure/data-tables';
import { odata, RestError, TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../../customError/CustomError';
@Injectable()
export class PilotInfoService {
private readonly partitionKey: string = 'info';
constructor(
// @InjectRepository(PilotInfo)
// private readonly pilotInfoRepository: Repository<PilotInfo>
private readonly tableService: TableService
) {}
constructor(private readonly tableService: TableService) {}
// async find(rowKey: string): Promise<PilotInfo> {
// return await this.pilotInfoRepository.find(this.partitionKey, rowKey);
// }
// async findAll(): Promise<PilotInfo[]> {
// return await this.pilotInfoRepository.findAll();
// }
async findAll(): Promise<PilotInfoEntity[]> {
try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const entities = await client.listEntities({
queryOptions: { filter: odata`PartitionKey eq 'pilot'` }
});
const pilots: PilotInfoEntity[] = [];
for await (const entity of entities) {
const pilot: PilotInfoEntity = {
partitionKey: entity.partitionKey,
rowKey: entity.rowKey,
id: entity.id.toString(),
name: entity.name.toString()
};
pilots.push(pilot);
}
return pilots;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
const client: TableClient =

View File

@@ -1,33 +1,40 @@
import { Body, Controller, HttpException, Post } from '@nestjs/common';
import { Body, Controller, Get, HttpException, Post } from '@nestjs/common';
import { PilotInfoService } from './info/pilot-info.service';
import { PilotInfoDto } from './info/pilot-info.dto';
import { TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../customError/CustomError';
import { PilotInfoEntity } from './info/pilot-info.entity';
@Controller('pilots')
export class PilotController {
constructor(private readonly pilotInfoService: PilotInfoService) {}
@Post()
async createPilot(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
@Get()
async findAll(): Promise<PilotInfoEntity[]> {
try {
const response: TableInsertEntityHeaders =
await this.pilotInfoService.create(pilotInfoData);
const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll();
console.log(`Not Broken: ${response}`);
return pilots;
} catch (error) {
const customError = error as CustomError;
console.log(customError.name);
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
// throw new HttpException({
// status: customError.statusCode,
// error: customError.message
// }, customError.statusCode, {
// cause: customError.name
// });
@Post()
async create(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
try {
const response: TableInsertEntityHeaders =
await this.pilotInfoService.create(pilotInfoData);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
}

View File

@@ -16,7 +16,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.6.5",
"@noahspan/noahspan-components": "^0.6.8",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",

View File

@@ -5,25 +5,11 @@ import { useAppContext } from './hooks/appContext/UseAppContext';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
import { useMsal } from '@azure/msal-react';
import SiteNav from './components/siteNav/SiteNav';
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
import { User } from '@microsoft/microsoft-graph-types';
type EventPayloadExtended = EventPayload & { accessToken: string };
const App: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient();
const appContext = useAppContext();
const { inProgress, instance } = useMsal();
const handleSignIn = async () => {
await instance.loginRedirect({
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
});
};
const handleSignOut = () => {
instance.logoutRedirect();
};
useEffect(() => {
const getFeatureFlags = async () => {
@@ -48,49 +34,9 @@ const App: React.FC<unknown> = () => {
getFeatureFlags();
}, []);
useEffect(() => {
const callback = instance.addEventCallback(
async (message: EventMessage) => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
try {
const eventPayload: EventPayloadExtended =
message.payload as EventPayloadExtended;
const response: AxiosResponse = await httpClient.get(
`api/userProfile`,
{
headers: {
Authorization: `Bearer ${eventPayload.accessToken}`
}
}
);
const userProfile: User = response.data;
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
}
}
}
);
return () => {
if (callback) {
instance.removeEventCallback(callback);
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
}
};
}, []);
return (
<div className="container mx-auto">
<SiteNav
handleSignIn={handleSignIn}
handleSignOut={handleSignOut}
inProgress={inProgress}
/>
<SiteNav />
<Routes>
{useFeatureFlag('flying-pilots')?.enabled && (
<Route path="/" element={<Pilots />} />

View File

@@ -154,6 +154,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
onClick: handlePeoplePickerOnClick
}}
loading={isPeoplePickerLoading}
data-testid="pilot-form-people-picker"
/>
)}
/>
@@ -181,6 +182,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
onChange={onChange}
value={value}
data-testid="pilot-form-address-input"
/>
)}
/>
@@ -207,6 +209,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
onChange={onChange}
value={value}
data-testid="pilot-form-city-input"
/>
)}
/>
@@ -231,6 +234,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
onChange={onChange}
value={value}
variant="outlined"
data-testid="pilot-form-state-dropdown"
/>
)}
/>
@@ -257,6 +261,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
onChange={onChange}
value={value}
data-testid="pilot-form-postal-code-input"
/>
)}
/>
@@ -288,6 +293,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
onChange={onChange}
value={value}
data-testid="pilot-form-email-input"
/>
)}
/>
@@ -319,6 +325,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/>
@@ -437,7 +444,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
className="flex items-center gap-3"
variant="outlined"
onClick={onOpenCloseDrawer}
data-testid="pilot-drawer-cancel-button"
data-testid="pilot-cancel-button"
>
<XmarkIcon size="lg" />
Cancel
@@ -449,6 +456,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
loading={isLoading}
variant="filled"
type="submit"
data-testid="pilot-save-button"
>
<SaveIcon size="lg" />
Save

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import PilotForm from '../pilotForm/PilotForm';
import {
Button,
@@ -16,133 +16,88 @@ import {
TrashIcon,
Typography
} from '@noahspan/noahspan-components';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
const Pilots: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient();
const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenCloseDrawer = () => {
setIsDrawerOpen(!isDrawerOpen);
};
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
type Pilot = {
partitionKey: string;
rowKey: string;
id: string;
name: string;
};
const data: Person[] = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 24,
visits: 100,
status: 'In Relationship',
progress: 50
},
{
firstName: 'tandy',
lastName: 'miller',
age: 40,
visits: 40,
status: 'Single',
progress: 80
},
{
firstName: 'joe',
lastName: 'dirte',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10
}
];
const columns: TableColumnDef[] = [
{
accessorKey: 'firstName',
header: 'First Name',
cell: ({ getValue }) => (
<div>
{getValue<string>().charAt(0).toUpperCase() +
getValue<string>().slice(1)}
</div>
)
},
{
accessorKey: 'lastName',
header: 'Last Name',
cell: ({ getValue }) => (
<div>
{getValue<string>().charAt(0).toUpperCase() +
getValue<string>().slice(1)}
</div>
)
},
{
accessorKey: 'age',
header: 'Age',
cellProps: {
className: 'text-right'
}
},
{
accessorKey: 'visits',
header: 'Visits',
cellProps: {
className: 'text-right'
}
},
{
accessorKey: 'status',
header: 'Status'
},
{
accessorKey: 'progress',
header: 'Progress',
cellProps: {
className: 'text-right'
}
},
{
id: 'actions',
header: 'Actions',
cellProps: {
className: 'text-center'
},
cell: () => {
return (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<IconButton variant="text">
<EllipsisVerticalIcon size="xl" />
</IconButton>
</div>
</MenuHandler>
<MenuList>
<MenuItem className="flex gap-3">
<PenIcon size="lg" />
Edit
</MenuItem>
<MenuItem className="flex gap-3">
<EyeIcon size="lg" />
View
</MenuItem>
<hr className="my-3" />
<MenuItem className="flex gap-3">
<TrashIcon size="lg" />
Delete
</MenuItem>
</MenuList>
</Menu>
);
},
enableSorting: false
accessorKey: 'name',
header: 'Name'
}
// {
// id: 'actions',
// header: 'Actions',
// cellProps: {
// className: 'text-center'
// },
// cell: () => {
// return (
// <Menu placement="bottom-end">
// <MenuHandler>
// <div>
// <IconButton variant="text">
// <EllipsisVerticalIcon size="xl" />
// </IconButton>
// </div>
// </MenuHandler>
// <MenuList>
// <MenuItem className="flex gap-3">
// <PenIcon size="lg" />
// Edit
// </MenuItem>
// <MenuItem className="flex gap-3">
// <EyeIcon size="lg" />
// View
// </MenuItem>
// <hr className="my-3" />
// <MenuItem className="flex gap-3">
// <TrashIcon size="lg" />
// Delete
// </MenuItem>
// </MenuList>
// </Menu>
// );
// },
// enableSorting: false
// }
];
useEffect(() => {
const getPilots = async () => {
try {
const accessToken: string = await getAccessToken();
const response: AxiosResponse = await httpClient.get(`api/pilots`, {
headers: {
Authorization: accessToken
}
});
console.log(response.data);
setPilots(response.data);
} catch (error) {
console.log(error);
}
};
getPilots();
}, []);
return (
<>
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6">
@@ -154,13 +109,13 @@ const Pilots: React.FC<unknown> = () => {
className="flex justify-center gap-3"
variant="filled"
onClick={onOpenCloseDrawer}
data-testid="add-pilot-button"
data-testid="pilot-add-button"
>
<PlusIcon size="lg" />
Add Pilot
</Button>
</div>
<Table defaultData={data} columns={columns}></Table>
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
</div>
<PilotForm
isDrawerOpen={isDrawerOpen}

View File

@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { ISiteNavProps } from './ISiteNavProps';
// import { Link as ReactRouterLink } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext';
@@ -13,25 +14,139 @@ import {
NavbarMenu,
NavbarItemProps,
PlaneIcon,
SignOutIcon,
Spinner,
Typography
} from '@noahspan/noahspan-components';
import { useIsAuthenticated } from '@azure/msal-react';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { User } from '@microsoft/microsoft-graph-types';
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
const SiteNav: React.FC<ISiteNavProps> = ({
handleSignIn,
handleSignOut,
inProgress
}: ISiteNavProps) => {
type EventPayloadExtended = EventPayload & { accessToken: string };
const SiteNav: React.FC<unknown> = () => {
const [loading, setLoading] = useState<boolean>(false);
const [userPhoto, setUserPhoto] = useState<string>();
const httpClient: AxiosInstance = useHttpClient();
const appContext = useAppContext();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const { inProgress, instance } = useMsal();
const navItems: NavbarItemProps[] = [
{
name: 'Pilots',
url: '#'
}
];
const handleSignIn = async () => {
await instance.loginRedirect({
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
});
};
const handleSignOut = () => {
instance.logoutRedirect();
};
const getUserProfile = async (accessToken: string): Promise<User> => {
try {
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
headers: {
Authorization: accessToken
}
});
const userProfile: User = response.data;
return userProfile;
} catch (error) {
throw new Error();
}
};
const getUserPhoto = async (accessToken: string): Promise<string> => {
try {
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
headers: {
Authorization: accessToken
},
responseType: 'arraybuffer'
});
const arrayBufferView = new Uint8Array(response.data);
const blob = new Blob([arrayBufferView], { type: 'image/png' });
const imageUrl = window.URL.createObjectURL(blob);
return imageUrl;
} catch (error) {
console.log(error);
throw new Error();
}
};
useEffect(() => {
const callback = instance.addEventCallback(
async (message: EventMessage) => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
try {
setLoading(true);
const eventPayload: EventPayloadExtended =
message.payload as EventPayloadExtended;
const userProfile: User = await getUserProfile(
eventPayload.accessToken
);
const userPhoto = await getUserPhoto(eventPayload.accessToken);
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
}
);
return () => {
if (callback) {
instance.removeEventCallback(callback);
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
}
};
}, []);
useEffect(() => {
const setUserProfile = async () => {
try {
setLoading(true);
const accessToken: string = await getAccessToken();
const userProfile = await getUserProfile(accessToken);
const userPhoto = await getUserPhoto(accessToken);
setUserPhoto(userPhoto);
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
if (
isAuthenticated &&
Object.keys(appContext.state.userProfile).length === 0
) {
setUserProfile();
}
}, [isAuthenticated]);
return (
<Navbar className="mt-6">
@@ -42,23 +157,47 @@ const SiteNav: React.FC<ISiteNavProps> = ({
<NavbarLinks items={navItems} />
<NavbarMenu>
<div className="flex items-center gap-2 hidden lg:inline-block">
{isAuthenticated && (
{!loading && isAuthenticated && (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<Button variant="text" size="sm">
<>
{/* {userPhoto && */}
{/* <img className='rounded-full' src={userPhoto} /> */}
{/* } */}
{/* {!userPhoto && */}
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]">
NS
</div>
{/* <div className="flex gap-2">
<div className='flex-none'>
</div>
<Button className='flex-1' variant="text" size="sm">
{appContext.state.userProfile.displayName}
</Button>
</div>
</div> */}
</>
</MenuHandler>
<MenuList>
{/* <MenuItem onClick={handleSignOut}> */}
<MenuItem>
<Typography variant="small">Sign Out</Typography>
<MenuItem onClick={handleSignOut}>
<Typography
className="flex justify-center gap-3"
variant="small"
>
<SignOutIcon size="lg" />
Sign Out
</Typography>
</MenuItem>
</MenuList>
</Menu>
)}
{loading && isAuthenticated && (
<div className="flex justify-center gap-3">
<Spinner size="xs" />
<Typography variant="small">Loading...</Typography>
</div>
)}
{!isAuthenticated && (
<Button
variant="text"

33
package-lock.json generated
View File

@@ -76,7 +76,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.6.5",
"@noahspan/noahspan-components": "^0.6.8",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",
@@ -3397,9 +3397,9 @@
"link": true
},
"node_modules/@noahspan/noahspan-components": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.6.5.tgz",
"integrity": "sha512-rqwjSWY1mKzqoWBCDNB3WfvLb43DVet2J7a64414wLU/uCrTn4Q3/zpZxtKSE1Squ/geub25AUN0Ke302/4KvQ==",
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.6.8.tgz",
"integrity": "sha512-X4ftKtH9/ICyOCRKUnPMAh/u27Bi391edSKvIwWOxxawyIEZj58whjB0RfgcHV0O/Ap84lGt7YYctZTTzJHBEQ==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
@@ -3455,6 +3455,17 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
},
"node_modules/@noble/hashes": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -9961,6 +9972,17 @@
"node": ">=0.10.0"
}
},
"node_modules/otpauth": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.2.tgz",
"integrity": "sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==",
"dependencies": {
"@noble/hashes": "1.4.0"
},
"funding": {
"url": "https://github.com/hectorm/otpauth?sponsor=1"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -13153,7 +13175,8 @@
"@cucumber/cucumber": "^10.7.0",
"@playwright/test": "^1.44.0",
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0"
"fs-extra": "^11.2.0",
"otpauth": "^9.3.2"
}
},
"tests/node_modules/fs-extra": {

View File

@@ -6,9 +6,18 @@ Feature: Pilots
When the user clicks the "Pilots" navbar link
Then the user is on the "Pilots" page
Scenario: Cancel Add Pilot drawer
Given the user is on the "Pilots" page
When the user clicks the New button
Then the pilot drawer is visible
When the user clicks the pilot drawer cancel button
Then the pilot drawer is no longer visible
# Scenario: Cancel Pilot drawer
# Given the user is on the "Pilots" page
# When the user clicks the New button
# Then the pilot drawer is visible
# When the user clicks the pilot drawer cancel button
# Then the pilot drawer is no longer visible
# Scenario: Add pilot
# Given the user is on the "Pilots" page
# When the user clicks the Add Pilot button
# And the pilot drawer is visible
# And the user enters the pilot's information
# And the user clicks the Save button
# Then the Add Pilot drawer is no longer visible
# And the pilot is in the Pilots list

View File

@@ -3,24 +3,28 @@ import { config } from '../support/config';
export class PilotsPage {
page: Page;
newButton: Locator;
pilotDrawer: Locator;
pilotDrawerCancelButton: Locator;
pilotAddButton: Locator;
pilotSaveButton: Locator;
pilotCancelButton: Locator;
constructor(page: Page) {
this.page = page;
this.newButton = page.getByTestId('new-pilot-button');
this.pilotDrawer = page.getByTestId('pilot-drawer');
this.pilotDrawerCancelButton = page.getByTestId(
'pilot-drawer-cancel-button'
);
this.pilotAddButton = page.getByTestId('add-pilot-button');
this.pilotSaveButton = page.getByTestId('save-pilot-button');
this.pilotCancelButton = page.getByTestId('pilot-form-cancel-button');
}
public async clickNewButton() {
await this.newButton.click();
public async clickAddPilotButton() {
await this.pilotAddButton.click();
}
public async clickSaveButton() {
await this.pilotSaveButton.click();
}
public async clickPilotDrawerCancelButton() {
await this.pilotDrawerCancelButton.click();
await this.pilotCancelButton.click();
}
}

View File

@@ -4,11 +4,27 @@ import { config } from '../support/config';
import { expect } from '@playwright/test';
import { PilotsPage } from '../pages/pilots.page';
When('the user clicks the New button', async function (this: ICustomWorld) {
When(
'the user clicks the Add Pilot button',
async function (this: ICustomWorld) {
const pilotsPage = new PilotsPage(this.page!);
await pilotsPage.clickNewButton();
});
await pilotsPage.clickAddPilotButton();
}
);
When(
`the user enters the pilot's information`,
async function (this: ICustomWorld) {
const pilotsPage = new PilotsPage(this.page!);
}
);
// When(`the user clicks the Save button`, async function (this: ICustomWorld) {
// const pilotsPage = new PilotsPage(this.page!);
// await pil
// })
When(
'the user clicks the pilot drawer cancel button',

View File

@@ -16,6 +16,7 @@
"@cucumber/cucumber": "^10.7.0",
"@playwright/test": "^1.44.0",
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0"
"fs-extra": "^11.2.0",
"otpauth": "^9.3.2"
}
}