From b4e274c355a6de57050c8735c40a04408ded6ebe Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Fri, 5 Jul 2024 10:13:45 -0600
Subject: [PATCH 01/54] chore: Update team member LinkedIn profile link (#519)
---
src/components/TeamSection/teamMembers.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/TeamSection/teamMembers.json b/src/components/TeamSection/teamMembers.json
index d7331c8..aea5807 100644
--- a/src/components/TeamSection/teamMembers.json
+++ b/src/components/TeamSection/teamMembers.json
@@ -54,7 +54,7 @@
"name": "Estefy Caballero",
"affiliation": "VP Communications",
"imagePath": "Estefy_Caballero.jpg",
- "linkedin": "https://www.linkedin.com/in/estefy-caballero-864aab25a"
+ "linkedin": "https://www.linkedin.com/in/estefanía-caballero-864aab25a/"
},
{
"name": "Esther Thompson",
From b557bc48c06df34eebcaef4f8496cc1f3268106e Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Fri, 23 Aug 2024 23:38:27 -0600
Subject: [PATCH 02/54] setup PlayWright tests
---
.gitignore | 6 +-
package-lock.json | 60 ++++
package.json | 1 +
playwright.config.ts | 78 +++++
tests-examples/demo-todo-app.spec.ts | 437 +++++++++++++++++++++++++++
tests/example.spec.ts | 18 ++
tests/homePage/HomePage.test.ts | 32 ++
7 files changed, 631 insertions(+), 1 deletion(-)
create mode 100644 playwright.config.ts
create mode 100644 tests-examples/demo-todo-app.spec.ts
create mode 100644 tests/example.spec.ts
create mode 100644 tests/homePage/HomePage.test.ts
diff --git a/.gitignore b/.gitignore
index 6ba12f6..0242a1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,8 @@ yarn-debug.log*
yarn-error.log*
# VSCode workspaces
-*.code-workspace
\ No newline at end of file
+*.code-workspace
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/package-lock.json b/package-lock.json
index b2676ef..c472722 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,7 @@
"web-vitals": "^1.1.2"
},
"devDependencies": {
+ "@playwright/test": "^1.46.1",
"@types/aos": "^3.0.4",
"@types/express": "^4.17.17",
"@types/jest": "^26.0.24",
@@ -4075,6 +4076,21 @@
"node": ">=14"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz",
+ "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.46.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
@@ -14562,6 +14578,50 @@
"node": ">=4"
}
},
+ "node_modules/playwright": {
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
+ "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.46.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
+ "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
+ "dev": true,
+ "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,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/popmotion": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
diff --git a/package.json b/package.json
index cbb0c5b..e55fee0 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
]
},
"devDependencies": {
+ "@playwright/test": "^1.46.1",
"@types/aos": "^3.0.4",
"@types/express": "^4.17.17",
"@types/jest": "^26.0.24",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..4a501ea
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,78 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default 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,
+ // },
+});
diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts
new file mode 100644
index 0000000..8641cb5
--- /dev/null
+++ b/tests-examples/demo-todo-app.spec.ts
@@ -0,0 +1,437 @@
+import { test, expect, type Page } from '@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'
+] as const;
+
+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: 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');
+ }
+}
+
+async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
+ return await page.waitForFunction(e => {
+ return JSON.parse(localStorage['react-todos']).length === e;
+ }, expected);
+}
+
+async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
+ return await page.waitForFunction(e => {
+ return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
+ }, expected);
+}
+
+async function checkTodosInLocalStorage(page: Page, title: string) {
+ return await page.waitForFunction(t => {
+ return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
+ }, title);
+}
diff --git a/tests/example.spec.ts b/tests/example.spec.ts
new file mode 100644
index 0000000..9daeac2
--- /dev/null
+++ b/tests/example.spec.ts
@@ -0,0 +1,18 @@
+import { test, expect } from '@playwright/test';
+
+test('has title', async ({ page }) => {
+ await page.goto('https://playwright.dev/');
+
+ // Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/enables/);
+});
+
+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();
+});
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
new file mode 100644
index 0000000..27046cc
--- /dev/null
+++ b/tests/homePage/HomePage.test.ts
@@ -0,0 +1,32 @@
+import { test, expect } from '@playwright/test';
+
+/**
+ * Test that logo shows up
+ */
+
+test('Should display Logo', async ({ page }) => {
+ await page.goto("http://localhost:3000");
+ // Find the logo image by class
+ const logo = page.locator('img.homePage__logo');
+ await expect(logo).toBeVisible();
+});
+
+
+/**
+ * Test join team button
+ */
+test('join button should navigate to Apply page', async function ({ page }) {
+ await page.goto("http://localhost:3000");
+
+ // find the join team button
+ page.getByRole('link', { name: 'theTeam.join()' }).click();
+ await expect(page).toHaveURL(/.*apply/)
+});
+
+/**
+ * Test checkout project button
+ */
+
+/**
+ * Test sponsorship button
+ */
\ No newline at end of file
From c2a1d84b46b2c63c1345c795e246e3238e4b9768 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 24 Aug 2024 14:32:16 -0600
Subject: [PATCH 03/54] Included more test cases
---
.../ProjectSection/ProjectsSection.tsx | 2 +-
tests/homePage/HomePage.test.ts | 41 ++++++++++++++++++-
2 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/src/components/ProjectSection/ProjectsSection.tsx b/src/components/ProjectSection/ProjectsSection.tsx
index 56ba45a..ffdead8 100644
--- a/src/components/ProjectSection/ProjectsSection.tsx
+++ b/src/components/ProjectSection/ProjectsSection.tsx
@@ -84,7 +84,7 @@ export const ProjectsSection = () => {
network with the tech community on campus.
- {
+ await page.goto("http://localhost:3000");
+
+ // find the button by locating the div class
+ const sponsorshipBtn = await page.getByRole('link', { name: 'Check out our sponsorship' });
+
+ //clicking the button creates a new tab, so we need switch to it
+ const [newPage] = await Promise.all([
+ page.waitForEvent('popup'),
+ sponsorshipBtn.click()
+ ])
+
+ await newPage.waitForLoadState('load');
+
+ await testInfo.attach("sponsorship_Pdf", {
+ body: await newPage.screenshot(),
+ contentType: "image/png",
+ })
+ await expect(newPage).toHaveTitle(/Sponsorship/)
+ await newPage.close();
+ await page.close();
+});
\ No newline at end of file
From b64dc116c09b0d63bc435ad5fa1ae72e79d4d3c0 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 24 Aug 2024 14:34:59 -0600
Subject: [PATCH 04/54] refactor: Updated documentation to include how to run
Playwright tests
---
content-updates.md | 55 +++++++++++++++++++++++++++++++---------------
1 file changed, 37 insertions(+), 18 deletions(-)
diff --git a/content-updates.md b/content-updates.md
index 7287efa..f622064 100644
--- a/content-updates.md
+++ b/content-updates.md
@@ -31,15 +31,11 @@ This guide documents how and when to update different content on the Tech Start
Statistics about our projects and members are set using the `NumberStat` component on the homepage:
```typescript
-
-
-
-
-
+
+
+
+
+
```
The `number` values must be updated at the end of each school year.
@@ -72,18 +68,21 @@ You can update our social media outlets via the `SocialMedia` component used in
For example:
```typescript
-
+
```
+
### Gallery
+
Everything related to gallery can be found under [src/components/PhotoGallery](https://github.com/Tech-Start-UCalgary/tsu-website/tree/main/src/components/PhotoGallery)
To Add a new picture visit [TechStart Google Photos Album](https://photos.app.goo.gl/SkVei5N56poqTh8g8)
- - Ask the administrator(s) for credential
- - click add new
+
+- Ask the administrator(s) for credential
+- click add new
To make changes to the server, visit [tsuServer](https://github.com/techstartucalgary/tsuServer)
@@ -172,12 +171,11 @@ The team picture should be updated after each showcase, stored in [src/images](h
Application status is managed through the `ApplySection` component used in `ApplyPage.tsx`.
-- When an application becomes open, set `statusIsOpen` to `true` and `applicationLink` to the new application URL.
+- When an application becomes open, set `statusIsOpen` to `true` and `applicationLink` to the new application URL.
- When an application becomes closed, set `statusIsOpen` to `false`.
Example:
-
```typescript
```
+
+## Plawright Tests
+### Installing Plawright
+Here are 2 helpful links to get you started with Plawright
+[Plawright Getting Started ](https://playwright.dev/docs/intro) Or [Getting Started with Playwright and VS Code](https://www.youtube.com/watch?v=Xz6lhEzgI5I)
+
+### How to run the tests
+This will run all the tests in the tests folder
+```bash
+npx playwright test
+```
+This will run the test cases in the HomePage.test.ts file on the chromium browser
+```bash
+npx playwright test tests/homePage/HomePage.test.ts --project=chromium
+```
+### To view the test results
+Either navigate to the `playwright-report` folder and open the generated `report.html` file in the browser\
+or run the following command to open the report.html file in the browser
+```bash
+npx playwright show-report
+```
From 1d655a90459142e710d36b4fe2fdded6f07e243a Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 24 Aug 2024 14:38:55 -0600
Subject: [PATCH 05/54] additonal updates to documentation
---
content-updates.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/content-updates.md b/content-updates.md
index f622064..28ab2bb 100644
--- a/content-updates.md
+++ b/content-updates.md
@@ -23,6 +23,10 @@ This guide documents how and when to update different content on the Tech Start
- [Applications](#applications)
- [Resources Page](#resources-page)
- [Guides](#guides)
+ - [Plawright Tests](#plawright-tests)
+ - [Installing Plawright](#installing-plawright)
+ - [How to run tests](#how-to-run-the-tests)
+ - [To view test results](#to-view-the-test-results)
## Homepage
@@ -220,22 +224,31 @@ Example:
```
## Plawright Tests
+
### Installing Plawright
+
Here are 2 helpful links to get you started with Plawright
[Plawright Getting Started ](https://playwright.dev/docs/intro) Or [Getting Started with Playwright and VS Code](https://www.youtube.com/watch?v=Xz6lhEzgI5I)
### How to run the tests
+
This will run all the tests in the tests folder
+
```bash
npx playwright test
```
+
This will run the test cases in the HomePage.test.ts file on the chromium browser
+
```bash
npx playwright test tests/homePage/HomePage.test.ts --project=chromium
```
+
### To view the test results
+
Either navigate to the `playwright-report` folder and open the generated `report.html` file in the browser\
or run the following command to open the report.html file in the browser
+
```bash
npx playwright show-report
```
From e548a21d60ec3a8dd2727a5ca9804d07b0ba90fd Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 7 Sep 2024 01:21:54 -0600
Subject: [PATCH 06/54] Added test coverage for navigation bar and project page
---
tests-examples/demo-todo-app.spec.ts | 437 ---------------------------
tests/example.spec.ts | 18 --
tests/homePage/HomePage.test.ts | 22 +-
tests/navigationBar/NavBar.test.ts | 134 ++++++++
tests/project/ProjectPage.test.ts | 13 +
5 files changed, 157 insertions(+), 467 deletions(-)
delete mode 100644 tests-examples/demo-todo-app.spec.ts
delete mode 100644 tests/example.spec.ts
create mode 100644 tests/navigationBar/NavBar.test.ts
create mode 100644 tests/project/ProjectPage.test.ts
diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts
deleted file mode 100644
index 8641cb5..0000000
--- a/tests-examples/demo-todo-app.spec.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-import { test, expect, type Page } from '@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'
-] as const;
-
-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: 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');
- }
-}
-
-async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
- return await page.waitForFunction(e => {
- return JSON.parse(localStorage['react-todos']).length === e;
- }, expected);
-}
-
-async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
- return await page.waitForFunction(e => {
- return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
- }, expected);
-}
-
-async function checkTodosInLocalStorage(page: Page, title: string) {
- return await page.waitForFunction(t => {
- return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
- }, title);
-}
diff --git a/tests/example.spec.ts b/tests/example.spec.ts
deleted file mode 100644
index 9daeac2..0000000
--- a/tests/example.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test('has title', async ({ page }) => {
- await page.goto('https://playwright.dev/');
-
- // Expect a title "to contain" a substring.
- await expect(page).toHaveTitle(/enables/);
-});
-
-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();
-});
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
index 9369fee..c5508a9 100644
--- a/tests/homePage/HomePage.test.ts
+++ b/tests/homePage/HomePage.test.ts
@@ -5,10 +5,12 @@ import { test, expect } from '@playwright/test';
*/
test('Should display Logo', async ({ page }) => {
- await page.goto("http://localhost:3000");
+ await page.goto("https://techstartucalgary.com");
// Find the logo image by class
const logo = page.locator('img.homePage__logo');
await expect(logo).toBeVisible();
+ await page.close();
+
});
@@ -16,7 +18,7 @@ test('Should display Logo', async ({ page }) => {
* Test join team button
*/
test('join button should navigate to Apply page', async function ({ page }) {
- await page.goto("http://localhost:3000");
+ await page.goto("https://techstartucalgary.com");
// find the join team button
await page.getByRole('link', { name: 'theTeam.join()' }).click();
@@ -28,25 +30,21 @@ test('join button should navigate to Apply page', async function ({ page }) {
* Test checkout project button
*/
test('join button should navigate to Project page', async function ({ page }) {
- await page.goto("http://localhost:3000");
-
- // find check out project button
- const firstLink = page.getByRole('link', { name: 'check out our projects!' }).first();
-
- // because of the way ProjectSection.tsx, is written there are 2 elements of the screen, but only 1 is visible at a time
- // I'm forcing clicking which ever one Playwright finds first(either the visible or the hidden element)
- await firstLink.click({ force: true });
+ await page.goto('https://techstartucalgary.com/');
+ await page.getByText('Check out our projects!').click();
+ await page.getByText('Our', { exact: true }).click();
+ await page.locator('#projectsPageTop').getByText('Projects', { exact: true }).click();
await expect(page).toHaveURL(/.*projects/);
-
await page.close();
});
+
/**
* Test sponsorship button
*/
test('sponsorship button should navigate to Sponsorship pdf ', async ({ page }, testInfo) => {
- await page.goto("http://localhost:3000");
+ await page.goto("https://techstartucalgary.com");
// find the button by locating the div class
const sponsorshipBtn = await page.getByRole('link', { name: 'Check out our sponsorship' });
diff --git a/tests/navigationBar/NavBar.test.ts b/tests/navigationBar/NavBar.test.ts
new file mode 100644
index 0000000..725cac8
--- /dev/null
+++ b/tests/navigationBar/NavBar.test.ts
@@ -0,0 +1,134 @@
+import { test, expect } from '@playwright/test';
+
+/**
+ * Test About button stays on the landing page
+ */
+test('Test About button stays on the landing page', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('About').click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/');
+ await page.close();
+
+});
+
+/**
+ * Test Team button navigates to the team page and shows Founder
+ */
+test('Test Team button navigates to the team page and shows Founder', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Team', { exact: true }).click();
+ await expect(page.getByRole('heading', { name: 'Our Team' })).toBeVisible();
+ await expect(page.locator('.FounderSectionstyles__FounderImg-sc-1pgvjm2-0')).toBeVisible();
+ await expect(page.getByText('Joel Happ')).toBeVisible();
+ await expect(page.getByText('Founder & Chairman')).toBeVisible();
+ await expect(page.getByText('Joel HappFounder &')).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test Team button navigates to the team page and shows previous presidents
+ */
+test('Test Team button navigates to the team page and shows the 3 previous presidents', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Team', { exact: true }).click();
+ await expect(page.getByRole('img', { name: '/static/media/Niyousha_Raeesinejad.7d226e07871c60102716.jpg' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Niyousha Raeesinejad' })).toBeVisible();
+ await expect(page.getByRole('img', { name: '/static/media/Tyler_Chan.' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Tyler Chan' })).toBeVisible();
+ await expect(page.getByRole('img', { name: '/static/media/Rajpreet_Gill.' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Rajpreet Gill' })).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test Team button navigates to the team page and shows current presidents
+ */
+test('Test Team button navigates to the team page and shows the 2 current presidents', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Team', { exact: true }).click();
+// await expect().toBeVisible();
+ await expect(page.getByRole('img', { name: '/static/media/Rachel_Renegado' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Rachel Renegado' })).toBeVisible();
+ await expect(page.getByRole('img', { name: '/static/media/Aarsh_Shah.' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Aarsh Shah' })).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test project button navigates to the project page
+ */
+test('Test project button navigates to the project page', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Projects', { exact: true }).click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/projects');
+ await expect(page.getByText('Our', { exact: true })).toBeVisible();
+ await expect(page.locator('#projectsPageTop').getByText('Projects', { exact: true })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Final Showcase Winners' })).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test apply button navigates to the Apply page
+ */
+test('Test apply button navigates to the apply page', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Apply', { exact: true }).click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/apply');
+ await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test merch button navigates to the Merch page
+ */
+test('Test merch button navigates to the Merch page and then find the 2 merch', async function({page}){
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Merch', { exact: true }).click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/merch');
+ await expect(page.getByRole('heading', { name: 'Original Basic Crewneck' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Crewneck with Sleeve Print' })).toBeVisible();
+ await page.close();
+
+});
+
+/**
+ * Test gallery button navigates to the Gallery page
+ */
+test('Test gallery button navigates to the Merch page', async function ({ page }) {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Gallery', { exact: true }).click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/gallery');
+ await page.getByRole('heading', { name: 'Gallery' }).click();
+ await expect(page.locator('.sc-beySbM > img').first()).toBeVisible();
+ await expect(page.locator('img:nth-child(15)')).toBeVisible();
+ await page.close();
+});
+
+/**
+ * Test resources button navigates to the Resources page
+ */
+test('Test resources button navigates to the Resources page', async function ({ page }) {
+ await page.goto('https://techstartucalgary.com/');
+ await page.locator('label').click();
+ await page.getByText('Resources', { exact: true }).click();
+ await expect(page).toHaveURL('https://techstartucalgary.com/resources');
+ await expect(page.getByText('Django Guide')).toBeVisible();
+ await expect(page.getByText('Git Guide')).toBeVisible();
+ await expect(page.getByText('Web Dev Guide')).toBeVisible();
+ await expect(page.getByText('React Guide')).toBeVisible();
+ await expect(page.getByText('API Guide')).toBeVisible();
+ await page.close();
+});
\ No newline at end of file
diff --git a/tests/project/ProjectPage.test.ts b/tests/project/ProjectPage.test.ts
new file mode 100644
index 0000000..4c16655
--- /dev/null
+++ b/tests/project/ProjectPage.test.ts
@@ -0,0 +1,13 @@
+import { test, expect } from '@playwright/test';
+
+/**
+ * Test 'Apply Now' nagivates to the apply now page
+ */
+test('Apply Now btn nagivates to the apply now page', async ({ page }) => {
+ await page.goto('https://techstartucalgary.com/projects');
+ await page.getByText('Apply Now').click();
+ await expect(page).toHaveURL(/.*apply/);
+ await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
+ await page.close();
+
+});
\ No newline at end of file
From baa9d4747b021bee0de18a0546f6ae3629b8b313 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Fri, 13 Sep 2024 17:24:28 -0600
Subject: [PATCH 07/54] updated PlayWright config for a larger viewport size.
And updated content-updates to include Playwright Recorder
Signed-off-by: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
---
content-updates.md | 13 +++++++++++--
playwright.config.ts | 15 ++++++++++++---
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/content-updates.md b/content-updates.md
index 28ab2bb..0da8e49 100644
--- a/content-updates.md
+++ b/content-updates.md
@@ -228,9 +228,10 @@ Example:
### Installing Plawright
Here are 2 helpful links to get you started with Plawright
-[Plawright Getting Started ](https://playwright.dev/docs/intro) Or [Getting Started with Playwright and VS Code](https://www.youtube.com/watch?v=Xz6lhEzgI5I)
+[Plawright Getting Started ](https://playwright.dev/docs/intro) Or [Getting Started with Playwright and VS Code](https://www.youtube.com/watch?v=Xz6lhEzgI5I).
+The second link is a video tutorial that will guide you through the process of setting up Plawright with VS Code, writing tests, and running them using the IDE.
-### How to run the tests
+### How to run the tests using the terminal
This will run all the tests in the tests folder
@@ -252,3 +253,11 @@ or run the following command to open the report.html file in the browser
```bash
npx playwright show-report
```
+
+### Create tests using Playwright Recorder
+
+You can either click "Record new" or use the terminal command `npx playwright codegen ` to generate test code for a specific URL and a specific viewport size.\
+For example, to generate test code for the Tech Start UCalgary website with a viewport size of 1600x900, run the following command:
+```bash
+npx playwright codegen --viewport-size=1600,900 https://techstartucalgary.com
+```
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
index 4a501ea..a456902 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -35,17 +35,26 @@ export default defineConfig({
projects: [
{
name: 'chromium',
- use: { ...devices['Desktop Chrome'] },
+ use: {
+ ...devices['Desktop Chrome'],
+ viewport: { width: 1600, height: 900 }, // Simulating 90% zoom by using larger resolution
+ },
},
{
name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
+ use: {
+ ...devices['Desktop Firefox'],
+ viewport: { width: 1600, height: 900 }, // Simulating 90% zoom by using larger resolution
+ },
},
{
name: 'webkit',
- use: { ...devices['Desktop Safari'] },
+ use: {
+ ...devices['Desktop Safari'],
+ viewport: { width: 1600, height: 900 }, // Simulating 90% zoom by using larger resolution
+ },
},
/* Test against mobile viewports. */
From 59cb69625e15a80b53a5ab573b618be0bd85dd42 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Fri, 13 Sep 2024 19:12:32 -0600
Subject: [PATCH 08/54] Tests that had to find image elements were based on
style-components which will regenerated each time. \n Hence, added
data-testid to PhotoGallery.tsx, Profile.tsx, FounderSection.styles.ts to
images could be uniquely identified based on an id.
Signed-off-by: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
---
src/components/PhotoGallery/PhotoGallery.tsx | 10 +++---
.../TeamFounder/FounderSection.styles.ts | 2 +-
src/components/TeamSection/Profile.tsx | 3 +-
tests/navigationBar/NavBar.test.ts | 35 +++++++------------
4 files changed, 22 insertions(+), 28 deletions(-)
diff --git a/src/components/PhotoGallery/PhotoGallery.tsx b/src/components/PhotoGallery/PhotoGallery.tsx
index 8bbedf9..45eec9b 100644
--- a/src/components/PhotoGallery/PhotoGallery.tsx
+++ b/src/components/PhotoGallery/PhotoGallery.tsx
@@ -9,6 +9,7 @@ import * as S from "../../pages/GalleryPage.styles";
const PhotoGallery = () => {
const [photosURL, setPhotosURL] = useState([]); // photos will be an array of objects
+
//memoize a getAlbum to prevent unnecessary re-renders.
const getAlbum = useCallback(async () => {
const galleryPicsURL = process.env.REACT_APP_PIC_API_URL;
@@ -62,10 +63,11 @@ const PhotoGallery = () => {
{photosURL.map((photo, index) => (
))}
diff --git a/src/components/TeamFounder/FounderSection.styles.ts b/src/components/TeamFounder/FounderSection.styles.ts
index bf3ef13..37cdfe9 100644
--- a/src/components/TeamFounder/FounderSection.styles.ts
+++ b/src/components/TeamFounder/FounderSection.styles.ts
@@ -4,7 +4,7 @@ interface TeamStyledProps {
mobileView: boolean;
}
-export const FounderImg = styled.img`
+export const FounderImg = styled.img.attrs({'data-testid': 'founder-img'})`
border-radius: 80%;
`;
diff --git a/src/components/TeamSection/Profile.tsx b/src/components/TeamSection/Profile.tsx
index 3144681..6c128f1 100644
--- a/src/components/TeamSection/Profile.tsx
+++ b/src/components/TeamSection/Profile.tsx
@@ -22,7 +22,7 @@ type ProfileProps = {
const Profile = (props: ProfileProps) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preventDragHandler = (e: any) => e.preventDefault();
-
+ const testDataId = `profile-image-${props.member.name.replace(/\s/g, "-").toLowerCase()}`;
return (
{
key={props.key}
alt={props.alt}
onDragStart={preventDragHandler}
+ data-testid={testDataId}
/>
diff --git a/tests/navigationBar/NavBar.test.ts b/tests/navigationBar/NavBar.test.ts
index 725cac8..a6d5fce 100644
--- a/tests/navigationBar/NavBar.test.ts
+++ b/tests/navigationBar/NavBar.test.ts
@@ -5,7 +5,6 @@ import { test, expect } from '@playwright/test';
*/
test('Test About button stays on the landing page', async ({ page }) => {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('About').click();
await expect(page).toHaveURL('https://techstartucalgary.com/');
await page.close();
@@ -16,11 +15,11 @@ test('Test About button stays on the landing page', async ({ page }) => {
* Test Team button navigates to the team page and shows Founder
*/
test('Test Team button navigates to the team page and shows Founder', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
+ // await page.goto('https://techstartucalgary.com/');
+ await page.goto('http://localhost:3000/')
await page.getByText('Team', { exact: true }).click();
await expect(page.getByRole('heading', { name: 'Our Team' })).toBeVisible();
- await expect(page.locator('.FounderSectionstyles__FounderImg-sc-1pgvjm2-0')).toBeVisible();
+ await expect(page.getByTestId('founder-img')).toBeVisible();
await expect(page.getByText('Joel Happ')).toBeVisible();
await expect(page.getByText('Founder & Chairman')).toBeVisible();
await expect(page.getByText('Joel HappFounder &')).toBeVisible();
@@ -33,13 +32,12 @@ test('Test Team button navigates to the team page and shows Founder', async ({ p
*/
test('Test Team button navigates to the team page and shows the 3 previous presidents', async ({ page }) => {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Team', { exact: true }).click();
- await expect(page.getByRole('img', { name: '/static/media/Niyousha_Raeesinejad.7d226e07871c60102716.jpg' })).toBeVisible();
+ await expect(page.getByTestId('profile-image-niyousha-raeesinejad')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Niyousha Raeesinejad' })).toBeVisible();
- await expect(page.getByRole('img', { name: '/static/media/Tyler_Chan.' })).toBeVisible();
+ await expect(page.getByTestId('profile-image-tyler-chan')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Tyler Chan' })).toBeVisible();
- await expect(page.getByRole('img', { name: '/static/media/Rajpreet_Gill.' })).toBeVisible();
+ await expect(page.getByTestId('profile-image-rajpreet-gill')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Rajpreet Gill' })).toBeVisible();
await page.close();
@@ -49,13 +47,11 @@ test('Test Team button navigates to the team page and shows the 3 previous presi
* Test Team button navigates to the team page and shows current presidents
*/
test('Test Team button navigates to the team page and shows the 2 current presidents', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
+ await page.goto('https://techstartucalgary.com/');
await page.getByText('Team', { exact: true }).click();
-// await expect().toBeVisible();
- await expect(page.getByRole('img', { name: '/static/media/Rachel_Renegado' })).toBeVisible();
+ await expect(page.getByTestId('profile-image-rachel-renegado')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Rachel Renegado' })).toBeVisible();
- await expect(page.getByRole('img', { name: '/static/media/Aarsh_Shah.' })).toBeVisible();
+ await expect(page.getByTestId('profile-image-aarsh-shah')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Aarsh Shah' })).toBeVisible();
await page.close();
@@ -66,7 +62,6 @@ test('Test Team button navigates to the team page and shows the 2 current presid
*/
test('Test project button navigates to the project page', async ({ page }) => {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Projects', { exact: true }).click();
await expect(page).toHaveURL('https://techstartucalgary.com/projects');
await expect(page.getByText('Our', { exact: true })).toBeVisible();
@@ -81,7 +76,6 @@ test('Test project button navigates to the project page', async ({ page }) => {
*/
test('Test apply button navigates to the apply page', async ({ page }) => {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Apply', { exact: true }).click();
await expect(page).toHaveURL('https://techstartucalgary.com/apply');
await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
@@ -92,9 +86,8 @@ test('Test apply button navigates to the apply page', async ({ page }) => {
/**
* Test merch button navigates to the Merch page
*/
-test('Test merch button navigates to the Merch page and then find the 2 merch', async function({page}){
+test('Test merch button navigates to the Merch page and then find the 2 merch', async function ({ page }) {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Merch', { exact: true }).click();
await expect(page).toHaveURL('https://techstartucalgary.com/merch');
await expect(page.getByRole('heading', { name: 'Original Basic Crewneck' })).toBeVisible();
@@ -104,16 +97,15 @@ test('Test merch button navigates to the Merch page and then find the 2 merch',
});
/**
- * Test gallery button navigates to the Gallery page
+ * Test that gallery button navigates to the Gallery page
*/
test('Test gallery button navigates to the Merch page', async function ({ page }) {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Gallery', { exact: true }).click();
await expect(page).toHaveURL('https://techstartucalgary.com/gallery');
await page.getByRole('heading', { name: 'Gallery' }).click();
- await expect(page.locator('.sc-beySbM > img').first()).toBeVisible();
- await expect(page.locator('img:nth-child(15)')).toBeVisible();
+ await expect(page.getByTestId('photo-gallery-image-0')).toBeVisible();
+ await expect(page.getByTestId('photo-gallery-image-32')).toBeVisible();
await page.close();
});
@@ -122,7 +114,6 @@ test('Test gallery button navigates to the Merch page', async function ({ page }
*/
test('Test resources button navigates to the Resources page', async function ({ page }) {
await page.goto('https://techstartucalgary.com/');
- await page.locator('label').click();
await page.getByText('Resources', { exact: true }).click();
await expect(page).toHaveURL('https://techstartucalgary.com/resources');
await expect(page.getByText('Django Guide')).toBeVisible();
From 09b4dbe8191d751bb52a15550800ce12d7255bcb Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 14 Sep 2024 14:26:13 -0600
Subject: [PATCH 09/54] memorized the testdataId for the profile image.
Signed-off-by: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
---
src/components/TeamSection/Profile.tsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/components/TeamSection/Profile.tsx b/src/components/TeamSection/Profile.tsx
index 6c128f1..f00924d 100644
--- a/src/components/TeamSection/Profile.tsx
+++ b/src/components/TeamSection/Profile.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useMemo } from "react";
import * as S from "./Profile.styles";
import { TeamMember } from "./TeamInformation";
import ProfileDescription from "./ProfileDescription";
@@ -22,7 +22,12 @@ type ProfileProps = {
const Profile = (props: ProfileProps) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preventDragHandler = (e: any) => e.preventDefault();
- const testDataId = `profile-image-${props.member.name.replace(/\s/g, "-").toLowerCase()}`;
+
+ // memoize the test data id so its not recalcaulated on every render but only when the member name changes
+ const testDataId = useMemo( () => {
+ console.log("memoizing test data id");
+ `profile-image-${props.member.name.replace(/\s/g, "-").toLowerCase()}`
+ }, [props.member.name]);
return (
Date: Sat, 14 Sep 2024 14:37:15 -0600
Subject: [PATCH 10/54] Inside useMemo, forgot to include return
---
src/components/TeamSection/Profile.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/components/TeamSection/Profile.tsx b/src/components/TeamSection/Profile.tsx
index f00924d..a9c6d04 100644
--- a/src/components/TeamSection/Profile.tsx
+++ b/src/components/TeamSection/Profile.tsx
@@ -25,8 +25,7 @@ const Profile = (props: ProfileProps) => {
// memoize the test data id so its not recalcaulated on every render but only when the member name changes
const testDataId = useMemo( () => {
- console.log("memoizing test data id");
- `profile-image-${props.member.name.replace(/\s/g, "-").toLowerCase()}`
+ return `profile-image-${props.member.name.replace(/\s/g, "-").toLowerCase()}`
}, [props.member.name]);
return (
From 383cc27fc77e70c07b83ae7e714b0e58f91e893e Mon Sep 17 00:00:00 2001
From: Sahiti Akella <63627104+Sahitiakella@users.noreply.github.com>
Date: Mon, 16 Sep 2024 13:07:07 -0600
Subject: [PATCH 11/54] Updated sponsor package link (#538)
Signed-off-by: s-akella04
Co-authored-by: s-akella04
---
src/components/SponsorSection/SponsorSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/SponsorSection/SponsorSection.tsx b/src/components/SponsorSection/SponsorSection.tsx
index 5111f89..e50c721 100644
--- a/src/components/SponsorSection/SponsorSection.tsx
+++ b/src/components/SponsorSection/SponsorSection.tsx
@@ -41,7 +41,7 @@ const SponsorSection = () => {
From 700bda0f33f6960f2b7c66ae1c5e0be48c75a83d Mon Sep 17 00:00:00 2001
From: brian nguyen
Date: Tue, 17 Sep 2024 11:25:58 -0700
Subject: [PATCH 12/54] Open Project Member applicaions for 24-25 year (#539)
Signed-off-by: brian-ngyn
---
src/pages/ApplyPage.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/pages/ApplyPage.tsx b/src/pages/ApplyPage.tsx
index b335ea4..bf519b7 100644
--- a/src/pages/ApplyPage.tsx
+++ b/src/pages/ApplyPage.tsx
@@ -251,9 +251,9 @@ const ApplyPage = () => {
role="Project Member"
description="Work and grow as a developer, business strategist, or designer.
Collaborate with team members on various platforms to build an exciting project for 1 academic year."
- statusIsOpen={false}
+ statusIsOpen={true}
closedStatus="APPLICATIONS CLOSED"
- applicationLink="https://forms.gle/Su73KRH6e5XF9BFd6"
+ applicationLink="https://forms.gle/aH3r7eKwk3sSHhwp8"
deadline=""
/>
@@ -261,7 +261,7 @@ const ApplyPage = () => {
role="Project Manager"
description="Manage a project for 1 year and lead a team of 6-9
project members to create complex projects."
- statusIsOpen={true}
+ statusIsOpen={false}
closedStatus="APPLICATIONS CLOSED"
applicationLink="https://forms.gle/sXrtiz5PQCLdwK6N7"
deadline=""
@@ -271,7 +271,7 @@ const ApplyPage = () => {
role="Executive Team"
description="Work behind the scenes for 1 academic year to organize project teams, run
workshops and events, and grow our club culture. Be a visionary that helps this club fulfill its goals!"
- statusIsOpen={true}
+ statusIsOpen={false}
closedStatus="APPLICATIONS CLOSED"
applicationLink="https://forms.gle/foZ76AGfBuUoLpNMA"
deadline=""
From 3acc0cfde8e0b8aef5a77c03c2e677c53c0ef7d9 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Wed, 18 Sep 2024 01:03:33 -0600
Subject: [PATCH 13/54] Add data-testid to ApplyButton component for unique
identification
---
src/components/ApplyButton.tsx | 2 +-
tests/homePage/HomePage.test.ts | 20 ++++++++++++++++++--
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/src/components/ApplyButton.tsx b/src/components/ApplyButton.tsx
index b3ea63f..4a8c9f5 100644
--- a/src/components/ApplyButton.tsx
+++ b/src/components/ApplyButton.tsx
@@ -32,7 +32,7 @@ const ApplyButton = () => {
>
>{" "}
- theTeam.join()
+ theTeam.join()
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
index c5508a9..0742fe4 100644
--- a/tests/homePage/HomePage.test.ts
+++ b/tests/homePage/HomePage.test.ts
@@ -18,10 +18,26 @@ test('Should display Logo', async ({ page }) => {
* Test join team button
*/
test('join button should navigate to Apply page', async function ({ page }) {
- await page.goto("https://techstartucalgary.com");
+// await page.goto("https://techstartucalgary.com");
+ await page.goto("http://localhost:3000");
+
+ // Wait for the join team button to be visible before clicking
+ await page.waitForSelector('[data-testid="homePage_apply_btn"]');
// find the join team button
- await page.getByRole('link', { name: 'theTeam.join()' }).click();
+ const applyBtn = page.getByTestId('homePage_apply_btn');
+ await applyBtn.scrollIntoViewIfNeeded();
+
+ // Click the button and wait for navigation
+ /* workaround for error:
+ waiting for element to be visible, enabled and stable
+- element is visible, enabled and stable
+ */
+ await Promise.all([
+ applyBtn.click(), // Trigger the click
+ await page.waitForURL(/.*apply/), // Wait for the navigation to /apply
+ ]);
+
await expect(page).toHaveURL(/.*apply/)
await page.close();
});
From 831bdbb58c7314b2ca9e1d8bf3efbb6640e6cf6b Mon Sep 17 00:00:00 2001
From: Morteza Faraji <111158753+mortezafa@users.noreply.github.com>
Date: Thu, 19 Sep 2024 17:01:24 -0600
Subject: [PATCH 14/54] handling empty PM list (#533)
* handling empty PM list
* Update src/components/TeamSection/TeamInformation.ts
Co-authored-by: Ben Schmidt
Signed-off-by: Morteza Faraji <111158753+mortezafa@users.noreply.github.com>
---------
Signed-off-by: Morteza Faraji <111158753+mortezafa@users.noreply.github.com>
Co-authored-by: Ben Schmidt
---
src/components/TeamSection/Profile.tsx | 47 ++++++++++---------
src/components/TeamSection/TeamInformation.ts | 20 ++++++--
src/components/TeamSection/teamMembers.json | 39 +--------------
3 files changed, 41 insertions(+), 65 deletions(-)
diff --git a/src/components/TeamSection/Profile.tsx b/src/components/TeamSection/Profile.tsx
index 3144681..5bb486a 100644
--- a/src/components/TeamSection/Profile.tsx
+++ b/src/components/TeamSection/Profile.tsx
@@ -30,29 +30,32 @@ const Profile = (props: ProfileProps) => {
data-aos-duration={!props.mobileView && "1000"}
mobileView={props.mobileView}
>
-
-
-
+ {props.profilePic && (
+
+
+
+ )}
-
-
-
+ {props.member.linkedin && (
+
+
+
+ )}
({
- id: index,
- ...pm,
- image: importImage(pm.imagePath)
-}));
+export const projectManagers: TeamMember[] = teamData.projectManagers?.length ?
+ teamData.projectManagers.map((pm: any, index: number) => ({
+ id: index,
+ name: pm.name,
+ affiliation: pm.affiliation,
+ image: importImage(pm.imagePath),
+ linkedin: pm.linkedin
+ })) :
+ [{
+ id: 0,
+ name: "Coming Soon",
+ affiliation: "",
+ image: "",
+ linkedin: ""
+ }];
export const boardMembers: TeamMember[] = teamData.boardMembers.map((bm, index) => ({
id: index,
diff --git a/src/components/TeamSection/teamMembers.json b/src/components/TeamSection/teamMembers.json
index aea5807..022310a 100644
--- a/src/components/TeamSection/teamMembers.json
+++ b/src/components/TeamSection/teamMembers.json
@@ -93,44 +93,7 @@
"linkedin": "https://www.linkedin.com/in/mehrnaz-zafari/"
}
],
- "projectManagers": [
- {
- "name": "Aarsh Shah",
- "affiliation": "CampusBuddy",
- "imagePath": "Aarsh_Shah.jpg",
- "linkedin": "https://www.linkedin.com/in/aarsh-shah-0a84161a9/"
- },
- {
- "name": "Hilton Luu",
- "affiliation": "LocaLoyalty",
- "imagePath": "Hilton_Luu.jpg",
- "linkedin": "https://www.linkedin.com/in/hilton-luu/"
- },
- {
- "name": "Lujaina Eldelebshany",
- "affiliation": "Fashion App",
- "imagePath": "Lujaina_ Eldelebshany.jpg",
- "linkedin": "https://www.linkedin.com/in/lujaina-eldelebshany-0029bb1b3/"
- },
- {
- "name": "Wilbur Elbouni",
- "affiliation": "Achevio",
- "imagePath": "Wilbur_Elbouni.jpg",
- "linkedin": "https://www.linkedin.com/in/wilbur-elbouni-3ba923213/"
- },
- {
- "name": "Naman Bhoj",
- "affiliation": "For Your Research",
- "imagePath": "Naman_Bhoj.jpg",
- "linkedin": "https://www.linkedin.com/in/naman-bhoj-3032a6154"
- },
- {
- "name": "Hamza Afzaal",
- "affiliation": "CraftXR",
- "imagePath": "Hamza_Afzaal.jpg",
- "linkedin": "https://www.linkedin.com/in/ammerhamza/"
- }
- ],
+ "projectManagers": [],
"boardMembers": [
{
"name": "Niyousha Raeesinejad",
From 9b6155b4f909c00907cea30f9ef081d7dfc8eae5 Mon Sep 17 00:00:00 2001
From: brian nguyen
Date: Fri, 20 Sep 2024 13:05:04 -0700
Subject: [PATCH 15/54] Labib Co-VP Finance (#542)
---
src/components/TeamSection/teamMembers.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/TeamSection/teamMembers.json b/src/components/TeamSection/teamMembers.json
index 022310a..bbb83d8 100644
--- a/src/components/TeamSection/teamMembers.json
+++ b/src/components/TeamSection/teamMembers.json
@@ -28,7 +28,7 @@
},
{
"name": "Labib Afshar Ahmed",
- "affiliation": "VP Finance",
+ "affiliation": "Co-VP Finance",
"imagePath": "Labib_Ahmed.jpg",
"linkedin": "https://www.linkedin.com/in/labib-afsar-ahmed/"
},
@@ -114,4 +114,4 @@
"linkedin": "https://www.linkedin.com/in/rajpreet-gill/"
}
]
-}
+}
\ No newline at end of file
From 977ec01400b925bb00019fe497d69e56d206e83c Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Mon, 23 Sep 2024 23:36:08 -0600
Subject: [PATCH 16/54] Added an env variable for testing, and a globalSetup
configuration file for running tests.
---
global-setup.ts | 15 +++++++++++
playwright.config.ts | 11 +++-----
tests/homePage/HomePage.test.ts | 9 +++----
tests/navigationBar/NavBar.test.ts | 43 +++++++++++++++---------------
tests/project/ProjectPage.test.ts | 4 +--
5 files changed, 46 insertions(+), 36 deletions(-)
create mode 100644 global-setup.ts
diff --git a/global-setup.ts b/global-setup.ts
new file mode 100644
index 0000000..519cd6a
--- /dev/null
+++ b/global-setup.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from '@playwright/test';
+import dotenv from 'dotenv';
+import path from 'path';
+
+/**
+ * globalSetup configuration file to set something up once before running all tests
+ */
+
+async function globalSetupFunc() {
+ // import dotenv from 'dotenv';
+ // read from ".env.testEnv file"
+ dotenv.config({ path: path.resolve(__dirname, '.env.local') });
+ console.log("Running tests on environment:", process.env.STAGING === '1' ? "Local" : "Production");
+}
+export default globalSetupFunc;
diff --git a/playwright.config.ts b/playwright.config.ts
index a456902..15e4e9a 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,16 +1,13 @@
import { defineConfig, devices } from '@playwright/test';
+import dotenv from 'dotenv';
+import path from 'path';
-/**
- * Read environment variables from file.
- * https://github.com/motdotla/dotenv
- */
-// import dotenv from 'dotenv';
-// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
+ globalSetup: require.resolve('./global-setup'),
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
@@ -26,7 +23,7 @@ export default defineConfig({
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
-
+ baseURL: process.env.STAGING === '1' ? "http://localhost:3000" : "https://techstartucalgary.com",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
index 0742fe4..70257af 100644
--- a/tests/homePage/HomePage.test.ts
+++ b/tests/homePage/HomePage.test.ts
@@ -5,7 +5,7 @@ import { test, expect } from '@playwright/test';
*/
test('Should display Logo', async ({ page }) => {
- await page.goto("https://techstartucalgary.com");
+ await page.goto("/");
// Find the logo image by class
const logo = page.locator('img.homePage__logo');
await expect(logo).toBeVisible();
@@ -18,8 +18,7 @@ test('Should display Logo', async ({ page }) => {
* Test join team button
*/
test('join button should navigate to Apply page', async function ({ page }) {
-// await page.goto("https://techstartucalgary.com");
- await page.goto("http://localhost:3000");
+ await page.goto("/");
// Wait for the join team button to be visible before clicking
await page.waitForSelector('[data-testid="homePage_apply_btn"]');
@@ -46,7 +45,7 @@ test('join button should navigate to Apply page', async function ({ page }) {
* Test checkout project button
*/
test('join button should navigate to Project page', async function ({ page }) {
- await page.goto('https://techstartucalgary.com/');
+ await page.goto('/');
await page.getByText('Check out our projects!').click();
await page.getByText('Our', { exact: true }).click();
await page.locator('#projectsPageTop').getByText('Projects', { exact: true }).click();
@@ -60,7 +59,7 @@ test('join button should navigate to Project page', async function ({ page }) {
* Test sponsorship button
*/
test('sponsorship button should navigate to Sponsorship pdf ', async ({ page }, testInfo) => {
- await page.goto("https://techstartucalgary.com");
+ await page.goto("/");
// find the button by locating the div class
const sponsorshipBtn = await page.getByRole('link', { name: 'Check out our sponsorship' });
diff --git a/tests/navigationBar/NavBar.test.ts b/tests/navigationBar/NavBar.test.ts
index a6d5fce..d5fc998 100644
--- a/tests/navigationBar/NavBar.test.ts
+++ b/tests/navigationBar/NavBar.test.ts
@@ -3,10 +3,10 @@ import { test, expect } from '@playwright/test';
/**
* Test About button stays on the landing page
*/
-test('Test About button stays on the landing page', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
+test('Test About button stays on the landing page', async ({ page, baseURL }) => {
+ await page.goto('/');
await page.getByText('About').click();
- await expect(page).toHaveURL('https://techstartucalgary.com/');
+ await expect(page).toHaveURL(`${baseURL}`);
await page.close();
});
@@ -15,8 +15,7 @@ test('Test About button stays on the landing page', async ({ page }) => {
* Test Team button navigates to the team page and shows Founder
*/
test('Test Team button navigates to the team page and shows Founder', async ({ page }) => {
- // await page.goto('https://techstartucalgary.com/');
- await page.goto('http://localhost:3000/')
+ await page.goto('/')
await page.getByText('Team', { exact: true }).click();
await expect(page.getByRole('heading', { name: 'Our Team' })).toBeVisible();
await expect(page.getByTestId('founder-img')).toBeVisible();
@@ -31,7 +30,7 @@ test('Test Team button navigates to the team page and shows Founder', async ({ p
* Test Team button navigates to the team page and shows previous presidents
*/
test('Test Team button navigates to the team page and shows the 3 previous presidents', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
+ await page.goto('/');
await page.getByText('Team', { exact: true }).click();
await expect(page.getByTestId('profile-image-niyousha-raeesinejad')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Niyousha Raeesinejad' })).toBeVisible();
@@ -47,7 +46,7 @@ test('Test Team button navigates to the team page and shows the 3 previous presi
* Test Team button navigates to the team page and shows current presidents
*/
test('Test Team button navigates to the team page and shows the 2 current presidents', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
+ await page.goto('/');
await page.getByText('Team', { exact: true }).click();
await expect(page.getByTestId('profile-image-rachel-renegado')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Rachel Renegado' })).toBeVisible();
@@ -60,10 +59,10 @@ test('Test Team button navigates to the team page and shows the 2 current presid
/**
* Test project button navigates to the project page
*/
-test('Test project button navigates to the project page', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
+test('Test project button navigates to the project page', async ({ page, baseURL }) => {
+ await page.goto('/');
await page.getByText('Projects', { exact: true }).click();
- await expect(page).toHaveURL('https://techstartucalgary.com/projects');
+ await expect(page).toHaveURL(`${baseURL}/projects`);
await expect(page.getByText('Our', { exact: true })).toBeVisible();
await expect(page.locator('#projectsPageTop').getByText('Projects', { exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Final Showcase Winners' })).toBeVisible();
@@ -74,10 +73,10 @@ test('Test project button navigates to the project page', async ({ page }) => {
/**
* Test apply button navigates to the Apply page
*/
-test('Test apply button navigates to the apply page', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/');
+test('Test apply button navigates to the apply page', async ({ page, baseURL }) => {
+ await page.goto('/');
await page.getByText('Apply', { exact: true }).click();
- await expect(page).toHaveURL('https://techstartucalgary.com/apply');
+ await expect(page).toHaveURL(`${baseURL}/apply`);
await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
await page.close();
@@ -86,10 +85,10 @@ test('Test apply button navigates to the apply page', async ({ page }) => {
/**
* Test merch button navigates to the Merch page
*/
-test('Test merch button navigates to the Merch page and then find the 2 merch', async function ({ page }) {
- await page.goto('https://techstartucalgary.com/');
+test('Test merch button navigates to the Merch page and then find the 2 merch', async function ({ page, baseURL }) {
+ await page.goto('/');
await page.getByText('Merch', { exact: true }).click();
- await expect(page).toHaveURL('https://techstartucalgary.com/merch');
+ await expect(page).toHaveURL(`${baseURL}/merch`);
await expect(page.getByRole('heading', { name: 'Original Basic Crewneck' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Crewneck with Sleeve Print' })).toBeVisible();
await page.close();
@@ -99,10 +98,10 @@ test('Test merch button navigates to the Merch page and then find the 2 merch',
/**
* Test that gallery button navigates to the Gallery page
*/
-test('Test gallery button navigates to the Merch page', async function ({ page }) {
- await page.goto('https://techstartucalgary.com/');
+test('Test gallery button navigates to the Merch page', async function ({ page, baseURL }) {
+ await page.goto('/');
await page.getByText('Gallery', { exact: true }).click();
- await expect(page).toHaveURL('https://techstartucalgary.com/gallery');
+ await expect(page).toHaveURL(`${ baseURL }/gallery`);
await page.getByRole('heading', { name: 'Gallery' }).click();
await expect(page.getByTestId('photo-gallery-image-0')).toBeVisible();
await expect(page.getByTestId('photo-gallery-image-32')).toBeVisible();
@@ -112,10 +111,10 @@ test('Test gallery button navigates to the Merch page', async function ({ page }
/**
* Test resources button navigates to the Resources page
*/
-test('Test resources button navigates to the Resources page', async function ({ page }) {
- await page.goto('https://techstartucalgary.com/');
+test('Test resources button navigates to the Resources page', async function ({ page, baseURL }) {
+ await page.goto('/');
await page.getByText('Resources', { exact: true }).click();
- await expect(page).toHaveURL('https://techstartucalgary.com/resources');
+ await expect(page).toHaveURL(`${baseURL}/resources`);
await expect(page.getByText('Django Guide')).toBeVisible();
await expect(page.getByText('Git Guide')).toBeVisible();
await expect(page.getByText('Web Dev Guide')).toBeVisible();
diff --git a/tests/project/ProjectPage.test.ts b/tests/project/ProjectPage.test.ts
index 4c16655..ba84968 100644
--- a/tests/project/ProjectPage.test.ts
+++ b/tests/project/ProjectPage.test.ts
@@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test';
/**
* Test 'Apply Now' nagivates to the apply now page
*/
-test('Apply Now btn nagivates to the apply now page', async ({ page }) => {
- await page.goto('https://techstartucalgary.com/projects');
+test('Apply Now btn nagivates to the apply now page', async ({ page, baseURL }) => {
+ await page.goto(`${baseURL}/projects`);
await page.getByText('Apply Now').click();
await expect(page).toHaveURL(/.*apply/);
await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
From 982fc9ad92928bf3b61707e840f1a5e9912d811a Mon Sep 17 00:00:00 2001
From: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
Date: Wed, 25 Sep 2024 17:32:41 -0600
Subject: [PATCH 17/54] updating link to executive application (#544)
---
src/pages/ApplyPage.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/pages/ApplyPage.tsx b/src/pages/ApplyPage.tsx
index bf519b7..9eeb7d6 100644
--- a/src/pages/ApplyPage.tsx
+++ b/src/pages/ApplyPage.tsx
@@ -271,9 +271,9 @@ const ApplyPage = () => {
role="Executive Team"
description="Work behind the scenes for 1 academic year to organize project teams, run
workshops and events, and grow our club culture. Be a visionary that helps this club fulfill its goals!"
- statusIsOpen={false}
+ statusIsOpen={true}
closedStatus="APPLICATIONS CLOSED"
- applicationLink="https://forms.gle/foZ76AGfBuUoLpNMA"
+ applicationLink="https://forms.gle/GtioLzJqeNcPYHjB6"
deadline=""
/>
From b108a08fcbf8c024233999a81178ac0f4f24a459 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 14:43:52 -0600
Subject: [PATCH 18/54] modify playwright.config.ts to take the value passed
from the GitHub Actions
---
playwright.config.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/playwright.config.ts b/playwright.config.ts
index 15e4e9a..a0b0da6 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -23,7 +23,7 @@ export default defineConfig({
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
- baseURL: process.env.STAGING === '1' ? "http://localhost:3000" : "https://techstartucalgary.com",
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
From ef36b3f0ac74f5a8b255f4e7500177f22c1af1ec Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 15:03:41 -0600
Subject: [PATCH 19/54] Add Playwright test workflow for Vercel deployment
Signed-off-by: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
---
.../workflows/playwright_tests_on_vercel.yml | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 .github/workflows/playwright_tests_on_vercel.yml
diff --git a/.github/workflows/playwright_tests_on_vercel.yml b/.github/workflows/playwright_tests_on_vercel.yml
new file mode 100644
index 0000000..d7032d1
--- /dev/null
+++ b/.github/workflows/playwright_tests_on_vercel.yml
@@ -0,0 +1,21 @@
+on:
+ deployment_status:
+
+jobs:
+ run-end2endTests:
+ # Only run if the deployment was successful
+ if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout the code from the repository
+ - uses: actions/checkout@v2
+
+ # Install project dependencies and Playwright (including browsers)
+ - name: Install dependencies
+ run: npm ci && npx playwright install --with deps
+
+ # Run Playwright tests
+ - name: Run tests
+ run: npx playwright test
+ env:
+ BASE_URL: ${{github.event.deployment_status.environment_url}}
From 7cc49c143c7991db1100f8c878b66cf55439ead8 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 15:14:00 -0600
Subject: [PATCH 20/54] Fix Playwright installation command in Vercel workflow
---
.github/workflows/playwright_tests_on_vercel.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/playwright_tests_on_vercel.yml b/.github/workflows/playwright_tests_on_vercel.yml
index d7032d1..8d9b964 100644
--- a/.github/workflows/playwright_tests_on_vercel.yml
+++ b/.github/workflows/playwright_tests_on_vercel.yml
@@ -12,7 +12,7 @@ jobs:
# Install project dependencies and Playwright (including browsers)
- name: Install dependencies
- run: npm ci && npx playwright install --with deps
+ run: npm ci && npx playwright install --with-deps
# Run Playwright tests
- name: Run tests
From 4c3081e1acfd7ee552b14aab1d17f3c11407eb38 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 15:40:51 -0600
Subject: [PATCH 21/54] Add workers to playwright test commands and uploaded
the test-reports
Signed-off-by: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
---
.github/workflows/playwright_tests_on_vercel.yml | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/playwright_tests_on_vercel.yml b/.github/workflows/playwright_tests_on_vercel.yml
index 8d9b964..a780d7c 100644
--- a/.github/workflows/playwright_tests_on_vercel.yml
+++ b/.github/workflows/playwright_tests_on_vercel.yml
@@ -16,6 +16,13 @@ jobs:
# Run Playwright tests
- name: Run tests
- run: npx playwright test
+ run: npx playwright test --workers=4
env:
BASE_URL: ${{github.event.deployment_status.environment_url}}
+
+ # Upload test result
+ - name: Upload Playwright test report
+ uses: actions/upload-artifact@v2
+ with:
+ name: playwright-reports
+ path: playwright-reports/
From 4185db21bca270a185cdce1bdc309365a42759ee Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 15:44:56 -0600
Subject: [PATCH 22/54] Update Playwright test report upload action to use
version 4 because v2 was deprecated
---
.github/workflows/playwright_tests_on_vercel.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/playwright_tests_on_vercel.yml b/.github/workflows/playwright_tests_on_vercel.yml
index a780d7c..d7d706b 100644
--- a/.github/workflows/playwright_tests_on_vercel.yml
+++ b/.github/workflows/playwright_tests_on_vercel.yml
@@ -22,7 +22,7 @@ jobs:
# Upload test result
- name: Upload Playwright test report
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: playwright-reports
path: playwright-reports/
From 80f97c0ff32a25f961bd1fe04c0eb25ae297ce10 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 17:06:05 -0600
Subject: [PATCH 23/54] Refactor Apply Now button in ProjectsPage.tsx and
test.ts files
---
src/pages/ProjectsPage.tsx | 16 +++++++++-------
tests/homePage/HomePage.test.ts | 9 +++++----
tests/navigationBar/NavBar.test.ts | 5 +++--
tests/project/ProjectPage.test.ts | 8 +++++---
4 files changed, 22 insertions(+), 16 deletions(-)
diff --git a/src/pages/ProjectsPage.tsx b/src/pages/ProjectsPage.tsx
index b5fd07c..adaa922 100644
--- a/src/pages/ProjectsPage.tsx
+++ b/src/pages/ProjectsPage.tsx
@@ -108,13 +108,15 @@ const ProjectsPage = () => {
bring to life with Tech Start?
-
+
+
+
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
index 70257af..e2e048b 100644
--- a/tests/homePage/HomePage.test.ts
+++ b/tests/homePage/HomePage.test.ts
@@ -16,8 +16,9 @@ test('Should display Logo', async ({ page }) => {
/**
* Test join team button
+ * sidenote: Oct 4,2024 -> consistently fails on webkit
*/
-test('join button should navigate to Apply page', async function ({ page }) {
+test('join button should navigate to Apply page', async function ({ page, baseURL }) {
await page.goto("/");
// Wait for the join team button to be visible before clicking
@@ -34,10 +35,10 @@ test('join button should navigate to Apply page', async function ({ page }) {
*/
await Promise.all([
applyBtn.click(), // Trigger the click
- await page.waitForURL(/.*apply/), // Wait for the navigation to /apply
+ await expect(page).toHaveURL(`${baseURL}/apply`) // Wait for the navigation to /apply
]);
-
- await expect(page).toHaveURL(/.*apply/)
+
+ await expect(page).toHaveURL(`${baseURL}/apply`);
await page.close();
});
diff --git a/tests/navigationBar/NavBar.test.ts b/tests/navigationBar/NavBar.test.ts
index d5fc998..f1b02a6 100644
--- a/tests/navigationBar/NavBar.test.ts
+++ b/tests/navigationBar/NavBar.test.ts
@@ -77,7 +77,8 @@ test('Test apply button navigates to the apply page', async ({ page, baseURL })
await page.goto('/');
await page.getByText('Apply', { exact: true }).click();
await expect(page).toHaveURL(`${baseURL}/apply`);
- await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
+ // ensure there's an application section
+ await expect(page.getByRole('heading', { name: 'Applications', exact: true })).toBeVisible()
await page.close();
});
@@ -101,7 +102,7 @@ test('Test merch button navigates to the Merch page and then find the 2 merch',
test('Test gallery button navigates to the Merch page', async function ({ page, baseURL }) {
await page.goto('/');
await page.getByText('Gallery', { exact: true }).click();
- await expect(page).toHaveURL(`${ baseURL }/gallery`);
+ await expect(page).toHaveURL(`${baseURL}/gallery`);
await page.getByRole('heading', { name: 'Gallery' }).click();
await expect(page.getByTestId('photo-gallery-image-0')).toBeVisible();
await expect(page.getByTestId('photo-gallery-image-32')).toBeVisible();
diff --git a/tests/project/ProjectPage.test.ts b/tests/project/ProjectPage.test.ts
index ba84968..0319e29 100644
--- a/tests/project/ProjectPage.test.ts
+++ b/tests/project/ProjectPage.test.ts
@@ -5,9 +5,11 @@ import { test, expect } from '@playwright/test';
*/
test('Apply Now btn nagivates to the apply now page', async ({ page, baseURL }) => {
await page.goto(`${baseURL}/projects`);
- await page.getByText('Apply Now').click();
- await expect(page).toHaveURL(/.*apply/);
- await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
+ // await page.getByText('Apply Now').click();
+ const applyButton = page.getByTestId('apply_now_button');
+ applyButton.click();
+ await expect(page).toHaveURL(`${baseURL}/apply`);
+ // await expect(page.getByRole('heading', { name: 'APPLY' })).toBeVisible();
await page.close();
});
\ No newline at end of file
From f87b285af50152ec23a16326d4580f17ebd0e886 Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 17:08:17 -0600
Subject: [PATCH 24/54] Refactor Playwright test workflow for Vercel deployment
---
.github/workflows/playwright_tests_on_vercel.yml | 2 +-
playwright.config.ts | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/playwright_tests_on_vercel.yml b/.github/workflows/playwright_tests_on_vercel.yml
index d7d706b..e368f06 100644
--- a/.github/workflows/playwright_tests_on_vercel.yml
+++ b/.github/workflows/playwright_tests_on_vercel.yml
@@ -16,7 +16,7 @@ jobs:
# Run Playwright tests
- name: Run tests
- run: npx playwright test --workers=4
+ run: npx playwright test --workers=4 --project=chromium
env:
BASE_URL: ${{github.event.deployment_status.environment_url}}
diff --git a/playwright.config.ts b/playwright.config.ts
index a0b0da6..a0fb7a7 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -18,7 +18,9 @@ export default defineConfig({
/* 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',
+ reporter: [
+ ['html', { outputFolder: 'playwright-reports', open: 'never' }],
+ ['list']],
/* 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('/')`. */
From a9686d8d2e95665b6661a571883673a720cfebdf Mon Sep 17 00:00:00 2001
From: IsaiahA21 <76446914+IsaiahA21@users.noreply.github.com>
Date: Sat, 5 Oct 2024 17:20:53 -0600
Subject: [PATCH 25/54] revert homepage test back
---
tests/homePage/HomePage.test.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/tests/homePage/HomePage.test.ts b/tests/homePage/HomePage.test.ts
index e2e048b..3b69708 100644
--- a/tests/homePage/HomePage.test.ts
+++ b/tests/homePage/HomePage.test.ts
@@ -16,9 +16,8 @@ test('Should display Logo', async ({ page }) => {
/**
* Test join team button
- * sidenote: Oct 4,2024 -> consistently fails on webkit
*/
-test('join button should navigate to Apply page', async function ({ page, baseURL }) {
+test('join button should navigate to Apply page', async function ({ page }) {
await page.goto("/");
// Wait for the join team button to be visible before clicking
@@ -35,10 +34,10 @@ test('join button should navigate to Apply page', async function ({ page, baseUR
*/
await Promise.all([
applyBtn.click(), // Trigger the click
- await expect(page).toHaveURL(`${baseURL}/apply`) // Wait for the navigation to /apply
+ await page.waitForURL(/.*apply/), // Wait for the navigation to /apply
]);
- await expect(page).toHaveURL(`${baseURL}/apply`);
+ await expect(page).toHaveURL(/.*apply/)
await page.close();
});
From 889bb642e9a36f0e25fcc76a396b2c1e1c5fb968 Mon Sep 17 00:00:00 2001
From: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
Date: Sun, 6 Oct 2024 17:57:26 -0600
Subject: [PATCH 26/54] 524 - initail changes, added Alumni tab, reusable
components (#536)
* 524 - initail changes, added Alumni tab, reusable components
* 524-Adding back Slider to UI, addressing comments. Signed-off-by: Sukriti, Website Developer
* Addressing comments - creating helper functions, fixing styles
Signed-off-by: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
* addressing comments - using var for bg color, using type for readability
Signed-off-by: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
* using types and correcting case
Signed-off-by: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
* using memoization
Signed-off-by: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
---------
Signed-off-by: Sukritib13 <113867416+Sukritib13@users.noreply.github.com>
Co-authored-by: brian nguyen
---
src/components/TeamSection/Profile.tsx | 23 +--
src/components/TeamSection/Team.tsx | 25 +++-
src/components/TeamSection/TeamInformation.ts | 6 +
.../TeamSection/TeamSection.styles.ts | 141 +++++++-----------
src/components/TeamSection/TeamSection.tsx | 47 +++---
src/components/TeamSection/teamMembers.json | 14 ++
src/images/team/Janita.jpg | Bin 0 -> 820848 bytes
src/images/team/Sajwal.jpeg | Bin 0 -> 690425 bytes
src/pages/TeamPage.tsx | 2 +-
src/utility/SharedStyles.tsx | 3 +
10 files changed, 141 insertions(+), 120 deletions(-)
create mode 100644 src/images/team/Janita.jpg
create mode 100644 src/images/team/Sajwal.jpeg
diff --git a/src/components/TeamSection/Profile.tsx b/src/components/TeamSection/Profile.tsx
index 5bb486a..511f530 100644
--- a/src/components/TeamSection/Profile.tsx
+++ b/src/components/TeamSection/Profile.tsx
@@ -14,10 +14,21 @@ type ProfileProps = {
profilePic: string;
alt: string;
member: TeamMember;
- isExec: boolean;
+ activeCategory: string;
};
-
+const getBackgroundColor = (category: string): SocialMediaColor => {
+ switch (category) {
+ case 'executives':
+ return SocialMediaColor.ToggleGreen;
+ case 'projectManagers':
+ return SocialMediaColor.ToggleBlue;
+ case 'alumni':
+ return SocialMediaColor.ToggleYellow;
+ default:
+ return SocialMediaColor.ToggleBlue;
+ }
+}
const Profile = (props: ProfileProps) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,13 +53,7 @@ const Profile = (props: ProfileProps) => {
)}
{props.member.linkedin && (
-
+
{
+ switch (category) {
+ case "executives":
+ return executiveTeam.length;
+ case "projectManagers":
+ return projectManagers.length;
+ case "alumni":
+ return alumniTeam.length;
+ default:
+ return 0;
+ }
};
+
const Team = (props: TeamProps) => {
return (
@@ -17,12 +32,12 @@ const Team = (props: TeamProps) => {
{props.teamMembers.map((member: TeamMember) => {
return (
);
})}
diff --git a/src/components/TeamSection/TeamInformation.ts b/src/components/TeamSection/TeamInformation.ts
index 624024b..809ed1c 100644
--- a/src/components/TeamSection/TeamInformation.ts
+++ b/src/components/TeamSection/TeamInformation.ts
@@ -47,6 +47,12 @@ export const projectManagers: TeamMember[] = teamData.projectManagers?.length ?
linkedin: ""
}];
+export const alumniTeam: TeamMember[] = teamData.alumniTeam.map((alum, index) => ({
+ id: index,
+ ...alum,
+ image: importImage(alum.imagePath)
+}));
+
export const boardMembers: TeamMember[] = teamData.boardMembers.map((bm, index) => ({
id: index,
...bm,
diff --git a/src/components/TeamSection/TeamSection.styles.ts b/src/components/TeamSection/TeamSection.styles.ts
index e88db8f..a70cf8a 100644
--- a/src/components/TeamSection/TeamSection.styles.ts
+++ b/src/components/TeamSection/TeamSection.styles.ts
@@ -1,4 +1,19 @@
import styled from "styled-components/macro";
+import { TeamCategory } from "./Team";
+
+// helper function
+const getStylesForCategory = (category: TeamCategory) => {
+ switch (category) {
+ case "executives":
+ return { backgroundColor: "var(--primary-green)", left: "5px" };
+ case "projectManagers":
+ return { backgroundColor: "var(--lightwash-green)", left: "calc(33.3% + 5px)" };
+ case "alumni":
+ return { backgroundColor: "var(--secondary-lime)", left: "calc(66.6% + 5px)" };
+ default:
+ return { backgroundColor: "var(--primary-green)", left: "5px" }; // Default to executives
+ }
+};
export const TeamSection = styled.div`
padding: 5vw;
@@ -19,102 +34,56 @@ export const TeamSection = styled.div`
`;
export const ToggleButtonWrapper = styled.div`
+ display: flex;
position: relative;
margin-top: 2%;
margin-left: -5%;
+ justify-content: center;
+ align-items: center;
`;
-export const ToggleButtonLabel = styled.label`
- overflow-y: hidden;
- display: inline-block;
+export const SliderWrapper = styled.div<{ selectedCategory: TeamCategory}>`
position: relative;
- @media (min-width: 351px) and (max-width: 475px) {
- width: 280px;
- height: 40px;
- margin-left: 0%;
- margin-top: 5%;
- }
- @media (min-width: 320px) and (max-width: 350px) {
- width: 280px;
- height: 40px;
- margin-left: 5%;
- margin-top: -5%;
- }
- margin-left: 0%;
- width: 425px;
- height: 50px;
+ display: flex;
+ justify-content: space-between;
+ width: 700px;
+ height: 60px;
+ background-color: ${({ selectedCategory }) =>
+ selectedCategory === "executives" ? "var(--primary-green)" :
+ selectedCategory === "projectManagers" ? "var(--lightwash-green)" :
+ "var(--secondary-lime)"};
border-radius: 50px;
- background: #4dd6a8;
- cursor: pointer;
- &::after {
- transform: translate(200px, -35px);
- content: "";
- display: block;
- border-radius: 50px;
- background-color: #49b893;
- @media (min-width: 320px) and (max-width: 475px) {
- width: 140px;
- height: 30px;
- margin-left: -65px;
- margin-top: 23px;
- }
- width: 210px;
- height: 40px;
- margin-left: 10px;
- margin-top: -57px;
- box-shadow: 1px 3px 3px 1px rgba(0, 0, 0, 0.2);
- transition: 0.8s;
- }
-`;
-
-export const ToggleButton = styled.input`
- opacity: 0;
- z-index: 1;
- border-radius: 15px;
- width: 42px;
- height: 26px;
- &:checked + ${ToggleButtonLabel} {
- background: #61c3d4;
- &::after {
- content: "";
- border-radius: 50px;
- background-color: #59acba;
- width: 200px;
- height: 40px;
- @media (min-width: 320px) and (max-width: 475px) {
- width: 130px;
- height: 30px;
- margin-left: 5px;
- margin-top: 23px;
- }
- margin-left: 5px;
- margin-top: -57px;
- transition: 0.8s;
- transform: translateY(-35px);
- }
- }
+ padding: 5px;
`;
-export const TeamCategoryText = styled.p`
- font-size: 1em;
- margin-left: -20px;
- font-family: "Inter", Tahoma, sans-serif;
- color: white;
+export const SliderOption = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ cursor: pointer;
+ padding: 10px 5px;
font-weight: 500;
-
- @media (min-width: 320px) and (max-width: 475px) {
- font-size: 0.65em;
- color: white;
- }
-`;
+ font-size: 1.5em;
+ color: #fff;
+ z-index: 2;
-export const Slider = styled.p`
- font-size: 1em;
- margin-left: 200px;
- height: 24px;
- @media (max-width: 475px) {
- height: 0;
- margin-top: -10px;
+ @media (max-width: 700px) {
+ font-size: 1.2em;
}
- font-weight: 500;
`;
+
+export const SliderPosition = styled.div<{ selectedCategory: TeamCategory }>`
+ position: absolute;
+ top: 5px;
+ left: ${({ selectedCategory }) =>
+ getStylesForCategory(selectedCategory).left};
+ width: calc(33.3% - 10px);
+ height: calc(100% - 10px);
+ background-color: ${({ selectedCategory }) =>
+ getStylesForCategory(selectedCategory).backgroundColor};
+ border-radius: 50px;
+ box-shadow: rgba(0, 0, 0, 0.2) 3px 3px 3px 3px;
+ transition: left 0.8s ease, background-color 0.2s ease-in-out;
+`;
\ No newline at end of file
diff --git a/src/components/TeamSection/TeamSection.tsx b/src/components/TeamSection/TeamSection.tsx
index 418348b..0bb0ff6 100644
--- a/src/components/TeamSection/TeamSection.tsx
+++ b/src/components/TeamSection/TeamSection.tsx
@@ -1,39 +1,48 @@
-import React from "react";
+import React, { useMemo } from "react";
import { useState } from "react";
import Team from "./Team";
import * as S from "./TeamSection.styles";
-import { executiveTeam, projectManagers } from "./TeamInformation";
+import { executiveTeam, projectManagers, alumniTeam } from "./TeamInformation";
+import { TeamCategory } from "./Team";
type TeamSectionProps = {
desktopView: boolean;
};
const TeamSection = (props: TeamSectionProps) => {
- const [toggle, setToggled] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState("executives");
- const getTeamMembers = () => {
- return toggle ? projectManagers : executiveTeam;
- };
+ const getTeamMembers = useMemo(() => {
+ switch (selectedCategory) {
+ case "projectManagers":
+ return projectManagers;
+ case "alumni":
+ return alumniTeam;
+ default:
+ return executiveTeam;
+ }
+ }, [selectedCategory]);
return (
- setToggled(prev => !prev)}
- />
-
-
- Project Managers Executives
-
-
-
+
+
+ setSelectedCategory("executives")}>
+ Executives
+
+ setSelectedCategory("projectManagers")}>
+ Project Managers
+
+ setSelectedCategory("alumni")}>
+ Alumni
+
+
);
diff --git a/src/components/TeamSection/teamMembers.json b/src/components/TeamSection/teamMembers.json
index bbb83d8..d2dafb8 100644
--- a/src/components/TeamSection/teamMembers.json
+++ b/src/components/TeamSection/teamMembers.json
@@ -94,6 +94,20 @@
}
],
"projectManagers": [],
+ "alumniTeam": [
+ {
+ "name": "Janita",
+ "affiliation": "Alumni VP Design",
+ "imagePath": "Janita.jpg",
+ "linkedin": "https://www.linkedin.com/in/janitamahum/"
+ },
+ {
+ "name": "Sajwal",
+ "affiliation": "Alumni VP Strategy",
+ "imagePath": "Sajwal.jpeg",
+ "linkedin": "https://www.linkedin.com/in/sajwal/"
+ }
+ ],
"boardMembers": [
{
"name": "Niyousha Raeesinejad",
diff --git a/src/images/team/Janita.jpg b/src/images/team/Janita.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..935c2063fd4d91083c04353479ca56cd8cc1614d
GIT binary patch
literal 820848
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%0Dqn#
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