On Deck + Misc Fixes and Changes (#1215)
* Added playwright and started writing e2e tests. * To make things easy, disabled other browsers while I get confortable. Added a login flow (assumes my dev env) * More tests on login page * Lots more testing code, trying to figure out auth code. * Ensure we don't track DBs inside config * Added a new date property for when chapters are added to a series which helps with OnDeck calculations. Changed a lot of heavy api calls to use IEnumerable to stream repsonse to UI. * Fixed OnDeck with a new field for when last chapter was added on Series. This is a streamlined way to query. Updated Reading List with NormalizedTitle, CoverImage, CoverImageLocked. * Implemented the ability to read a random item in the reading list and for the reading list to be intact for order. * Tweaked the style for webtoon to not span the whole width, but use max width * When we update a cover image just send an event so we don't need to have logic for when updates occur * Fixed a bad name for entity type on cover updates * Aligned the edit collection tag modal to align with new tab design * Rewrote code for picking the first file for metadata to ensure it always picks the correct file, esp if the first chapter of a series starts with a float (1.1) * Refactored setting LastChapterAdded to ensure we do it on the Series. * Updated Chapter updating in scan loop to avoid nested for loop and an additional loop. * Fixed a bug where locked person fields wouldn't persist between scans. * Updated Contributing to reflect how to view the swagger api
This commit is contained in:
parent
912dfa8a80
commit
3bbb02f574
64 changed files with 3397 additions and 343 deletions
27
UI/Web/.github/workflows/playwright.yml
vendored
Normal file
27
UI/Web/.github/workflows/playwright.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
3
UI/Web/.gitignore
vendored
Normal file
3
UI/Web/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
|
@ -20,7 +20,9 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
|||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
~~Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).~~
|
||||
|
||||
Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests.
|
||||
|
||||
## Further help
|
||||
|
||||
|
|
4
UI/Web/adminStorageState.json
Normal file
4
UI/Web/adminStorageState.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"cookies": [],
|
||||
"origins": []
|
||||
}
|
398
UI/Web/e2e/example.spec.ts.txt
Normal file
398
UI/Web/e2e/example.spec.ts.txt
Normal file
|
@ -0,0 +1,398 @@
|
|||
// import { test, expect, 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'
|
||||
// ];
|
||||
|
||||
// test.describe('New Todo', () => {
|
||||
// test('should allow me to add todo items', async ({ page }) => {
|
||||
// // Create 1st todo.
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[0]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
|
||||
// // Make sure the list only has one todo item.
|
||||
// await expect(page.locator('.view label')).toHaveText([
|
||||
// TODO_ITEMS[0]
|
||||
// ]);
|
||||
|
||||
// // Create 2nd todo.
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[1]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
|
||||
// // Make sure the list now has two todo items.
|
||||
// await expect(page.locator('.view label')).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 one todo item.
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[0]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
|
||||
// // Check that input is empty.
|
||||
// await expect(page.locator('.new-todo')).toBeEmpty();
|
||||
// await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
// });
|
||||
|
||||
// test('should append new items to the bottom of the list', async ({ page }) => {
|
||||
// // Create 3 items.
|
||||
// await createDefaultTodos(page);
|
||||
|
||||
// // Check test using different methods.
|
||||
// await expect(page.locator('.todo-count')).toHaveText('3 items left');
|
||||
// await expect(page.locator('.todo-count')).toContainText('3');
|
||||
// await expect(page.locator('.todo-count')).toHaveText(/3/);
|
||||
|
||||
// // Check all items in one call.
|
||||
// await expect(page.locator('.view label')).toHaveText(TODO_ITEMS);
|
||||
// await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
// });
|
||||
|
||||
// test('should show #main and #footer when items added', async ({ page }) => {
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[0]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
|
||||
// await expect(page.locator('.main')).toBeVisible();
|
||||
// await expect(page.locator('.footer')).toBeVisible();
|
||||
// await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
// });
|
||||
// });
|
||||
|
||||
// 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.locator('.toggle-all').check();
|
||||
|
||||
// // Ensure all todos have 'completed' class.
|
||||
// await expect(page.locator('.todo-list li')).toHaveClass(['completed', 'completed', 'completed']);
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
// });
|
||||
|
||||
// test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||
// // Check and then immediately uncheck.
|
||||
// await page.locator('.toggle-all').check();
|
||||
// await page.locator('.toggle-all').uncheck();
|
||||
|
||||
// // Should be no completed classes.
|
||||
// await expect(page.locator('.todo-list li')).toHaveClass(['', '', '']);
|
||||
// });
|
||||
|
||||
// test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||
// const toggleAll = page.locator('.toggle-all');
|
||||
// await toggleAll.check();
|
||||
// await expect(toggleAll).toBeChecked();
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// // Uncheck first todo.
|
||||
// const firstTodo = page.locator('.todo-list li').nth(0);
|
||||
// await firstTodo.locator('.toggle').uncheck();
|
||||
|
||||
// // Reuse toggleAll locator and make sure its not checked.
|
||||
// await expect(toggleAll).not.toBeChecked();
|
||||
|
||||
// await firstTodo.locator('.toggle').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 two items.
|
||||
// for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
// await page.locator('.new-todo').fill(item);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
// }
|
||||
|
||||
// // Check first item.
|
||||
// const firstTodo = page.locator('.todo-list li').nth(0);
|
||||
// await firstTodo.locator('.toggle').check();
|
||||
// await expect(firstTodo).toHaveClass('completed');
|
||||
|
||||
// // Check second item.
|
||||
// const secondTodo = page.locator('.todo-list li').nth(1);
|
||||
// await expect(secondTodo).not.toHaveClass('completed');
|
||||
// await secondTodo.locator('.toggle').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 two items.
|
||||
// for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
// await page.locator('.new-todo').fill(item);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
// }
|
||||
|
||||
// const firstTodo = page.locator('.todo-list li').nth(0);
|
||||
// const secondTodo = page.locator('.todo-list li').nth(1);
|
||||
// await firstTodo.locator('.toggle').check();
|
||||
// await expect(firstTodo).toHaveClass('completed');
|
||||
// await expect(secondTodo).not.toHaveClass('completed');
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// await firstTodo.locator('.toggle').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.locator('.todo-list li');
|
||||
// const secondTodo = todoItems.nth(1);
|
||||
// await secondTodo.dblclick();
|
||||
// await expect(secondTodo.locator('.edit')).toHaveValue(TODO_ITEMS[1]);
|
||||
// await secondTodo.locator('.edit').fill('buy some sausages');
|
||||
// await secondTodo.locator('.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.locator('.todo-list li').nth(1);
|
||||
// await todoItem.dblclick();
|
||||
// await expect(todoItem.locator('.toggle')).not.toBeVisible();
|
||||
// await expect(todoItem.locator('label')).not.toBeVisible();
|
||||
// await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
// });
|
||||
|
||||
// test('should save edits on blur', async ({ page }) => {
|
||||
// const todoItems = page.locator('.todo-list li');
|
||||
// await todoItems.nth(1).dblclick();
|
||||
// await todoItems.nth(1).locator('.edit').fill('buy some sausages');
|
||||
// await todoItems.nth(1).locator('.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.locator('.todo-list li');
|
||||
// await todoItems.nth(1).dblclick();
|
||||
// await todoItems.nth(1).locator('.edit').fill(' buy some sausages ');
|
||||
// await todoItems.nth(1).locator('.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.locator('.todo-list li');
|
||||
// await todoItems.nth(1).dblclick();
|
||||
// await todoItems.nth(1).locator('.edit').fill('');
|
||||
// await todoItems.nth(1).locator('.edit').press('Enter');
|
||||
|
||||
// await expect(todoItems).toHaveText([
|
||||
// TODO_ITEMS[0],
|
||||
// TODO_ITEMS[2],
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// test('should cancel edits on escape', async ({ page }) => {
|
||||
// const todoItems = page.locator('.todo-list li');
|
||||
// await todoItems.nth(1).dblclick();
|
||||
// await todoItems.nth(1).locator('.edit').press('Escape');
|
||||
// await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||
// });
|
||||
// });
|
||||
|
||||
// test.describe('Counter', () => {
|
||||
// test('should display the current number of todo items', async ({ page }) => {
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[0]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
// await expect(page.locator('.todo-count')).toContainText('1');
|
||||
|
||||
// await page.locator('.new-todo').fill(TODO_ITEMS[1]);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
// await expect(page.locator('.todo-count')).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.locator('.clear-completed')).toHaveText('Clear completed');
|
||||
// });
|
||||
|
||||
// test('should remove completed items when clicked', async ({ page }) => {
|
||||
// const todoItems = page.locator('.todo-list li');
|
||||
// await todoItems.nth(1).locator('.toggle').check();
|
||||
// await page.locator('.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.locator('.clear-completed').click();
|
||||
// await expect(page.locator('.clear-completed')).toBeHidden();
|
||||
// });
|
||||
// });
|
||||
|
||||
// test.describe('Persistence', () => {
|
||||
// test('should persist its data', async ({ page }) => {
|
||||
// for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
// await page.locator('.new-todo').fill(item);
|
||||
// await page.locator('.new-todo').press('Enter');
|
||||
// }
|
||||
|
||||
// const todoItems = page.locator('.todo-list li');
|
||||
// await todoItems.nth(0).locator('.toggle').check();
|
||||
// await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
// await expect(todoItems).toHaveClass(['completed', '']);
|
||||
|
||||
// // Ensure there is 1 completed item.
|
||||
// checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// // Now reload.
|
||||
// await page.reload();
|
||||
// await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
// 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 }) => {
|
||||
// await page.locator('.todo-list li .toggle').nth(1).check();
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
// await page.locator('.filters >> text=Active').click();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(2);
|
||||
// await expect(page.locator('.todo-list li')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
// });
|
||||
|
||||
// test('should respect the back button', async ({ page }) => {
|
||||
// await page.locator('.todo-list li .toggle').nth(1).check();
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// await test.step('Showing all items', async () => {
|
||||
// await page.locator('.filters >> text=All').click();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(3);
|
||||
// });
|
||||
|
||||
// await test.step('Showing active items', async () => {
|
||||
// await page.locator('.filters >> text=Active').click();
|
||||
// });
|
||||
|
||||
// await test.step('Showing completed items', async () => {
|
||||
// await page.locator('.filters >> text=Completed').click();
|
||||
// });
|
||||
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(1);
|
||||
// await page.goBack();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(2);
|
||||
// await page.goBack();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(3);
|
||||
// });
|
||||
|
||||
// test('should allow me to display completed items', async ({ page }) => {
|
||||
// await page.locator('.todo-list li .toggle').nth(1).check();
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
// await page.locator('.filters >> text=Completed').click();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(1);
|
||||
// });
|
||||
|
||||
// test('should allow me to display all items', async ({ page }) => {
|
||||
// await page.locator('.todo-list li .toggle').nth(1).check();
|
||||
// await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
// await page.locator('.filters >> text=Active').click();
|
||||
// await page.locator('.filters >> text=Completed').click();
|
||||
// await page.locator('.filters >> text=All').click();
|
||||
// await expect(page.locator('.todo-list li')).toHaveCount(3);
|
||||
// });
|
||||
|
||||
// test('should highlight the currently applied filter', async ({ page }) => {
|
||||
// await expect(page.locator('.filters >> text=All')).toHaveClass('selected');
|
||||
// await page.locator('.filters >> text=Active').click();
|
||||
// // Page change - active items.
|
||||
// await expect(page.locator('.filters >> text=Active')).toHaveClass('selected');
|
||||
// await page.locator('.filters >> text=Completed').click();
|
||||
// // Page change - completed items.
|
||||
// await expect(page.locator('.filters >> text=Completed')).toHaveClass('selected');
|
||||
// });
|
||||
// });
|
||||
|
||||
// async function createDefaultTodos(page: Page) {
|
||||
// for (const item of TODO_ITEMS) {
|
||||
// await page.locator('.new-todo').fill(item);
|
||||
// await page.locator('.new-todo').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(i => i.completed).length === e;
|
||||
// }, expected);
|
||||
// }
|
||||
|
||||
// async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||
// return await page.waitForFunction(t => {
|
||||
// return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t);
|
||||
// }, title);
|
||||
// }
|
|
@ -1,23 +0,0 @@
|
|||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('kavita-webui app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
18
UI/Web/e2e/src/app.spec.ts
Normal file
18
UI/Web/e2e/src/app.spec.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('When not authenticated, should be redirected to login page', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/', { waitUntil: 'networkidle' });
|
||||
expect(page.url()).toBe('http://localhost:4200/login');
|
||||
});
|
||||
|
||||
test('When not authenticated, should be redirected to login page from an authenticated page', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/library', { waitUntil: 'networkidle' });
|
||||
expect(page.url()).toBe('http://localhost:4200/login');
|
||||
});
|
||||
|
||||
// Not sure how to test when we need localStorage: https://github.com/microsoft/playwright/issues/6258
|
||||
// test('When authenticated, should be redirected to library page', async ({ page }) => {
|
||||
// await page.goto('http://localhost:4200/', { waitUntil: 'networkidle' });
|
||||
// console.log('url: ', page.url());
|
||||
// expect(page.url()).toBe('http://localhost:4200/library');
|
||||
// });
|
43
UI/Web/e2e/src/login/login.spec.ts
Normal file
43
UI/Web/e2e/src/login/login.spec.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test('When not authenticated, should be redirected to login page', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/', { waitUntil: 'networkidle' });
|
||||
expect(page.url()).toBe('http://localhost:4200/login');
|
||||
});
|
||||
|
||||
test('Should be able to log in', async ({ page }) => {
|
||||
|
||||
await page.goto('http://localhost:4200/login', { waitUntil: 'networkidle' });
|
||||
const username = page.locator('#username');
|
||||
expect(username).toBeEditable();
|
||||
const password = page.locator('#password');
|
||||
expect(password).toBeEditable();
|
||||
|
||||
await username.type('Joe');
|
||||
await password.type('P4ssword');
|
||||
|
||||
const button = page.locator('button[type="submit"]');
|
||||
await button.click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(200);
|
||||
expect(page.url()).toBe('http://localhost:4200/library');
|
||||
});
|
||||
|
||||
test('Should get a toastr when no username', async ({ page }) => {
|
||||
|
||||
await page.goto('http://localhost:4200/login', { waitUntil: 'networkidle' });
|
||||
const username = page.locator('#username');
|
||||
expect(username).toBeEditable();
|
||||
|
||||
await username.type('');
|
||||
|
||||
const button = page.locator('button[type="submit"]');
|
||||
await button.click();
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
const toastr = page.locator('#toast-container div[role="alertdialog"]')
|
||||
await expect(toastr).toHaveText('Invalid username');
|
||||
|
||||
expect(page.url()).toBe('http://localhost:4200/login');
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test('When on login page, clicking Forgot Password should redirect', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/login', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.click('a[routerlink="/registration/reset-password"]')
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
expect(page.url()).toBe('http://localhost:4200/registration/reset-password');
|
||||
});
|
||||
|
||||
test('Going directly to reset url should stay on the page', async ({page}) => {
|
||||
await page.goto('http://localhost:4200/registration/reset-password', { waitUntil: 'networkidle' });
|
||||
const email = page.locator('#email');
|
||||
expect(email).toBeEditable();
|
||||
})
|
||||
|
||||
test('Submitting an email, should give a prompt to user, redirect back to login', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/registration/reset-password', { waitUntil: 'networkidle' });
|
||||
|
||||
const email = page.locator('#email');
|
||||
expect(email).toBeEditable();
|
||||
|
||||
await email.type('XXX@gmail.com');
|
||||
|
||||
const button = page.locator('button[type="submit"]');
|
||||
await button.click();
|
||||
|
||||
const toastr = page.locator('#toast-container div[role="alertdialog"]')
|
||||
await expect(toastr).toHaveText('An email will be sent to the email if it exists in our database');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
expect(page.url()).toBe('http://localhost:4200/login');
|
||||
});
|
13
UI/Web/e2e/src/side-nav/side-nav.spec.ts
Normal file
13
UI/Web/e2e/src/side-nav/side-nav.spec.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.use({ storageState: 'storage/admin.json' });
|
||||
|
||||
test('When on login page, side nav should not render', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/login', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator(".side-nav")).toHaveCount(0)
|
||||
});
|
||||
|
||||
test('When on library page, side nav should render', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200/library', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator(".side-nav")).toHaveCount(1)
|
||||
});
|
46
UI/Web/global-setup.ts
Normal file
46
UI/Web/global-setup.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Browser, chromium, FullConfig, request } from '@playwright/test';
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
let requestContext = await request.newContext();
|
||||
var token = await requestContext.post('http://localhost:5000/account/login', {
|
||||
form: {
|
||||
'user': 'Joe',
|
||||
'password': 'P4ssword'
|
||||
}
|
||||
});
|
||||
console.log(token.json());
|
||||
// Save signed-in state to 'storageState.json'.
|
||||
//await requestContext.storageState({ path: 'adminStorageState.json' });
|
||||
await requestContext.dispose();
|
||||
|
||||
requestContext = await request.newContext();
|
||||
await requestContext.post('http://localhost:5000/account/login', {
|
||||
form: {
|
||||
'user': 'nonadmin',
|
||||
'password': 'P4ssword'
|
||||
}
|
||||
});
|
||||
// Save signed-in state to 'storageState.json'.
|
||||
//await requestContext.storageState({ path: 'nonAdminStorageState.json' });
|
||||
await requestContext.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// async function globalSetup (config: FullConfig) {
|
||||
// const browser = await chromium.launch()
|
||||
// await saveStorage(browser, 'nonadmin', 'P4ssword', 'storage/user.json')
|
||||
// await saveStorage(browser, 'Joe', 'P4ssword', 'storage/admin.json')
|
||||
// await browser.close()
|
||||
// }
|
||||
|
||||
async function saveStorage (browser: Browser, username: string, password: string, saveStoragePath: string) {
|
||||
const page = await browser.newPage()
|
||||
await page.goto('http://localhost:5000/account/login')
|
||||
await page.type('#username', username)
|
||||
await page.type('#password', password)
|
||||
await page.click('button[type="submit"]')
|
||||
await page.context().storageState({ path: saveStoragePath })
|
||||
}
|
||||
|
||||
export default globalSetup;
|
4
UI/Web/nonAdminStorageState.json
Normal file
4
UI/Web/nonAdminStorageState.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"cookies": [],
|
||||
"origins": []
|
||||
}
|
526
UI/Web/package-lock.json
generated
526
UI/Web/package-lock.json
generated
|
@ -1599,6 +1599,17 @@
|
|||
"@babel/helper-plugin-utils": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-typescript": {
|
||||
"version": "7.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz",
|
||||
"integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.16.7",
|
||||
"@babel/helper-plugin-utils": "^7.16.7",
|
||||
"@babel/plugin-syntax-typescript": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz",
|
||||
|
@ -1721,6 +1732,17 @@
|
|||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"@babel/preset-typescript": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz",
|
||||
"integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.16.7",
|
||||
"@babel/helper-validator-option": "^7.16.7",
|
||||
"@babel/plugin-transform-typescript": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||
|
@ -2553,6 +2575,278 @@
|
|||
"read-package-json-fast": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.20.2.tgz",
|
||||
"integrity": "sha512-unkLa+xe/lP7MVC0qpgadc9iSG1+LEyGBzlXhGS/vLGAJaSFs8DNfI89hNd5shHjWfNzb34JgPVnkRKCSNo5iw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.16.7",
|
||||
"@babel/core": "7.16.12",
|
||||
"@babel/helper-plugin-utils": "7.16.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-dynamic-import": "7.16.7",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-proposal-private-methods": "7.16.11",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.16.7",
|
||||
"@babel/plugin-syntax-async-generators": "7.8.4",
|
||||
"@babel/plugin-syntax-json-strings": "7.8.3",
|
||||
"@babel/plugin-syntax-object-rest-spread": "7.8.3",
|
||||
"@babel/plugin-syntax-optional-catch-binding": "7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "7.16.8",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "4.3.3",
|
||||
"expect": "27.2.5",
|
||||
"jest-matcher-utils": "27.2.5",
|
||||
"json5": "2.2.1",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "3.0.4",
|
||||
"ms": "2.1.3",
|
||||
"open": "8.4.0",
|
||||
"pirates": "4.0.4",
|
||||
"playwright-core": "1.20.2",
|
||||
"rimraf": "3.0.2",
|
||||
"source-map-support": "0.4.18",
|
||||
"stack-utils": "2.0.5",
|
||||
"yazl": "2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.16.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz",
|
||||
"integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.16.8",
|
||||
"@babel/helper-compilation-targets": "^7.16.7",
|
||||
"@babel/helper-module-transforms": "^7.16.7",
|
||||
"@babel/helpers": "^7.16.7",
|
||||
"@babel/parser": "^7.16.12",
|
||||
"@babel/template": "^7.16.7",
|
||||
"@babel/traverse": "^7.16.10",
|
||||
"@babel/types": "^7.16.8",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.1.2",
|
||||
"semver": "^6.3.0",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz",
|
||||
"integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"dev": true
|
||||
},
|
||||
"expect": {
|
||||
"version": "27.2.5",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-27.2.5.tgz",
|
||||
"integrity": "sha512-ZrO0w7bo8BgGoP/bLz+HDCI+0Hfei9jUSZs5yI/Wyn9VkG9w8oJ7rHRgYj+MA7yqqFa0IwHA3flJzZtYugShJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^27.2.5",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"jest-get-type": "^27.0.6",
|
||||
"jest-matcher-utils": "^27.2.5",
|
||||
"jest-message-util": "^27.2.5",
|
||||
"jest-regex-util": "^27.0.6"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"jest-matcher-utils": {
|
||||
"version": "27.2.5",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.5.tgz",
|
||||
"integrity": "sha512-qNR/kh6bz0Dyv3m68Ck2g1fLW5KlSOUNcFQh87VXHZwWc/gY6XwnKofx76Qytz3x5LDWT09/2+yXndTkaG4aWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0",
|
||||
"jest-diff": "^27.2.5",
|
||||
"jest-get-type": "^27.0.6",
|
||||
"pretty-format": "^27.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"dev": true
|
||||
},
|
||||
"mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"pirates": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz",
|
||||
"integrity": "sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.2.tgz",
|
||||
"integrity": "sha512-iV6+HftSPalynkq0CYJala1vaTOq7+gU9BRfKCdM9bAxNq/lFLrwbluug2Wt5OoUwbMABcnTThIEm3/qUhCdJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "4.3.3",
|
||||
"extract-zip": "2.0.1",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"jpeg-js": "0.4.3",
|
||||
"mime": "3.0.0",
|
||||
"pixelmatch": "5.2.1",
|
||||
"pngjs": "6.0.0",
|
||||
"progress": "2.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"proxy-from-env": "1.1.0",
|
||||
"rimraf": "3.0.2",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.4.2",
|
||||
"yauzl": "2.10.0",
|
||||
"yazl": "2.5.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.4.18",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
|
||||
"integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@polka/url": {
|
||||
"version": "1.0.0-next.21",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
||||
|
@ -2919,6 +3213,16 @@
|
|||
"integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/yauzl": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
|
||||
"integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
|
@ -3906,6 +4210,12 @@
|
|||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
@ -4237,6 +4547,12 @@
|
|||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"dev": true
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
|
@ -5113,6 +5429,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz",
|
||||
|
@ -5558,6 +5883,29 @@
|
|||
"tmp": "^0.0.33"
|
||||
}
|
||||
},
|
||||
"extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yauzl": "^2.9.1",
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
|
@ -5639,6 +5987,15 @@
|
|||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"fetch-cookie": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz",
|
||||
|
@ -8301,6 +8658,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
|
||||
"integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -9897,6 +10260,12 @@
|
|||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true
|
||||
},
|
||||
"pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
|
||||
"dev": true
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
|
@ -9955,6 +10324,23 @@
|
|||
"nice-napi": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"pixelmatch": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz",
|
||||
"integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pngjs": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pngjs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz",
|
||||
"integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
||||
|
@ -9964,6 +10350,86 @@
|
|||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.20.2.tgz",
|
||||
"integrity": "sha512-p6GE8A/f2G7t8FIk/AwQ94nT7R7tyPRJyKt1FwRjwBDf4WdpgoAr4hDfMgHy+CkClR22adFjopGwhxXAPsewhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"playwright-core": "1.20.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"dev": true
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.2.tgz",
|
||||
"integrity": "sha512-iV6+HftSPalynkq0CYJala1vaTOq7+gU9BRfKCdM9bAxNq/lFLrwbluug2Wt5OoUwbMABcnTThIEm3/qUhCdJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "4.3.3",
|
||||
"extract-zip": "2.0.1",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"jpeg-js": "0.4.3",
|
||||
"mime": "3.0.0",
|
||||
"pixelmatch": "5.2.1",
|
||||
"pngjs": "6.0.0",
|
||||
"progress": "2.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"proxy-from-env": "1.1.0",
|
||||
"rimraf": "3.0.2",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.4.2",
|
||||
"yauzl": "2.10.0",
|
||||
"yazl": "2.5.1"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
||||
"dev": true
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
|
@ -10366,6 +10832,12 @@
|
|||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true
|
||||
},
|
||||
"promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
|
@ -10400,6 +10872,25 @@
|
|||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"protractor": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz",
|
||||
|
@ -10761,6 +11252,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
|
@ -10773,6 +11270,16 @@
|
|||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
@ -12953,6 +13460,25 @@
|
|||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
"integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA=="
|
||||
},
|
||||
"yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"yazl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
|
||||
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-crc32": "~0.2.3"
|
||||
}
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
|
|
|
@ -49,12 +49,14 @@
|
|||
"@angular-devkit/build-angular": "~13.2.3",
|
||||
"@angular/cli": "^13.2.3",
|
||||
"@angular/compiler-cli": "~13.2.2",
|
||||
"@playwright/test": "^1.20.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"codelyzer": "^6.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"jest-preset-angular": "^11.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"playwright": "^1.20.2",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~10.5.0",
|
||||
"tslint": "^6.1.3",
|
||||
|
|
106
UI/Web/playwright.config.ts
Normal file
106
UI/Web/playwright.config.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './e2e',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* 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: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:4200',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
globalSetup: require.resolve('./global-setup'),
|
||||
|
||||
/* 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: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -19,5 +19,6 @@ export interface ReadingList {
|
|||
title: string;
|
||||
summary: string;
|
||||
promoted: boolean;
|
||||
coverImageLocked: boolean;
|
||||
items: Array<ReadingListItem>;
|
||||
}
|
|
@ -32,7 +32,7 @@ export interface SeriesMetadata {
|
|||
tagsLocked: boolean;
|
||||
writersLocked: boolean;
|
||||
coverArtistsLocked: boolean;
|
||||
publishersLocked: boolean;
|
||||
publisherLocked: boolean;
|
||||
charactersLocked: boolean;
|
||||
pencillersLocked: boolean;
|
||||
inkersLocked: boolean;
|
||||
|
|
|
@ -460,7 +460,7 @@ export class ActionService implements OnDestroy {
|
|||
}
|
||||
|
||||
editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) {
|
||||
const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'md' });
|
||||
const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg' });
|
||||
readingListModalRef.componentInstance.readingList = readingList;
|
||||
readingListModalRef.closed.pipe(take(1)).subscribe((list) => {
|
||||
if (callback && list !== undefined) {
|
||||
|
|
|
@ -75,6 +75,10 @@ export class ImageService implements OnDestroy {
|
|||
return this.baseUrl + 'image/collection-cover?collectionTagId=' + collectionTagId;
|
||||
}
|
||||
|
||||
getReadingListCoverImage(readingListId: number) {
|
||||
return this.baseUrl + 'image/readinglist-cover?readingListId=' + readingListId;
|
||||
}
|
||||
|
||||
getChapterCoverImage(chapterId: number) {
|
||||
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId;
|
||||
}
|
||||
|
|
|
@ -133,9 +133,6 @@ export class SeriesService {
|
|||
getRecentlyUpdatedSeries() {
|
||||
return this.httpClient.post<SeriesGroup[]>(this.baseUrl + 'series/recently-updated-series', {});
|
||||
}
|
||||
getRecentlyAddedChapters() {
|
||||
return this.httpClient.post<RecentlyAddedItem[]>(this.baseUrl + 'series/recently-added-chapters', {});
|
||||
}
|
||||
|
||||
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
|
||||
const data = this.createSeriesFilter(filter);
|
||||
|
|
|
@ -30,6 +30,10 @@ export class UploadService {
|
|||
return this.httpClient.post<number>(this.baseUrl + 'upload/collection', {id: tagId, url: this._cleanBase64Url(url)});
|
||||
}
|
||||
|
||||
updateReadingListCoverImage(readingListId: number, url: string) {
|
||||
return this.httpClient.post<number>(this.baseUrl + 'upload/reading-list', {id: readingListId, url: this._cleanBase64Url(url)});
|
||||
}
|
||||
|
||||
updateChapterCoverImage(chapterId: number, url: string) {
|
||||
return this.httpClient.post<number>(this.baseUrl + 'upload/chapter', {id: chapterId, url: this._cleanBase64Url(url)});
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
<span class="d-none d-sm-block"> {{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}}</span>
|
||||
</button>
|
||||
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back"><i class="fa fa-reply" aria-hidden="true"></i><span class="d-none d-sm-block"> Go Back</span></button>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()"><i class="fa fa-bars" aria-hidden="true"></i><span class="d-none d-sm-block">Settings {{drawerOpen}}</span></button>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()"><i class="fa fa-bars" aria-hidden="true"></i><span class="d-none d-sm-block">Settings</span></button>
|
||||
<div class="book-title col-2 d-none d-sm-block">
|
||||
<ng-container *ngIf="isLoading; else showTitle">
|
||||
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
|
||||
|
|
|
@ -5,16 +5,15 @@
|
|||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
|
||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]">
|
||||
<a ngbNavLink>{{tabs[0]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
<form [formGroup]="collectionTagForm">
|
||||
<div class="mb-3">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
|
@ -62,7 +61,7 @@
|
|||
</ul>
|
||||
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3 ms-2"></div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -4,6 +4,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
|||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { SelectionModel } from 'src/app/typeahead/typeahead.component';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
|
@ -14,6 +15,7 @@ import { LibraryService } from 'src/app/_services/library.service';
|
|||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
|
@ -39,11 +41,15 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService) { }
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
public utilityService: UtilityService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.pagination == undefined) {
|
||||
|
|
|
@ -192,8 +192,8 @@
|
|||
<div class="mb-3">
|
||||
<label for="publisher" class="form-label">Publisher</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
|
||||
[(locked)]="metadata.publishersLocked" (onUnlock)="metadata.publishersLocked = false"
|
||||
(newItemAdded)="metadata.publishersLocked = true" (selectedData)="metadata.publishersLocked = true">
|
||||
[(locked)]="metadata.publisherLocked" (onUnlock)="metadata.publisherLocked = false"
|
||||
(newItemAdded)="metadata.publisherLocked = true" (selectedData)="metadata.publisherLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<!-- TODO: Refactor this so we can use series actions here -->
|
||||
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
|
@ -27,11 +26,4 @@
|
|||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
|
||||
<!-- <app-carousel-reel [items]="recentlyAddedChapters" title="Recently Added" (sectionClick)="handleSectionClick($event)">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" [title]="item.title" [subtitle]="item.seriesName" [imageUrl]="imageService.getRecentlyAddedItem(item)"
|
||||
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel> -->
|
||||
</app-carousel-reel>
|
|
@ -29,7 +29,6 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
isAdmin = false;
|
||||
|
||||
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||
recentlyAddedChapters: RecentlyAddedItem[] = [];
|
||||
inProgress: Series[] = [];
|
||||
recentlyAddedSeries: Series[] = [];
|
||||
|
||||
|
@ -57,7 +56,6 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
this.recentlyAddedChapters = this.recentlyAddedChapters.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
} else if (res.event === EVENTS.ScanSeries) {
|
||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||
this.loadRecentlyAdded$.next();
|
||||
|
@ -125,10 +123,6 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
this.seriesService.getRecentlyUpdatedSeries().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries;
|
||||
});
|
||||
|
||||
this.seriesService.getRecentlyAddedChapters().pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
|
||||
this.recentlyAddedChapters = updatedSeries;
|
||||
});
|
||||
}
|
||||
|
||||
handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
||||
|
|
|
@ -27,13 +27,10 @@
|
|||
|
||||
|
||||
img, .full-width {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// .img-container {
|
||||
// overflow: auto;
|
||||
// }
|
||||
|
||||
|
||||
@keyframes move-up-down {
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{readingList.title}} Reading List</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
This list is currently {{readingList?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the list can be seen server-wide, not just for admin users. All series that are within this list will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
<form [formGroup]="reviewGroup">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Name</label>
|
||||
<input id="title" class="form-control" formControlName="title" type="text">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]">
|
||||
<a ngbNavLink>{{tabs[0].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
This list is currently {{readingList?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the list can be seen server-wide, not just for admin users. All series that are within this list will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
<form [formGroup]="reviewGroup">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Name</label>
|
||||
<input id="title" class="form-control" formControlName="title" type="text">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[1]">
|
||||
<a ngbNavLink>{{tabs[1].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="readingList.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="ms-2 mt-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-reading-list-modal',
|
||||
|
@ -14,13 +20,33 @@ export class EditReadingListModalComponent implements OnInit {
|
|||
@Input() readingList!: ReadingList;
|
||||
reviewGroup!: FormGroup;
|
||||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService) { }
|
||||
coverImageIndex: number = 0;
|
||||
/**
|
||||
* Url of the selected cover
|
||||
*/
|
||||
selectedCover: string = '';
|
||||
coverImageLocked: boolean = false;
|
||||
|
||||
imageUrls: Array<string> = [];
|
||||
|
||||
tabs = [{title: 'General', disabled: false}, {title: 'Cover', disabled: false}];
|
||||
active = this.tabs[0];
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private imageService: ImageService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reviewGroup = new FormGroup({
|
||||
title: new FormControl(this.readingList.title, [Validators.required]),
|
||||
summary: new FormControl(this.readingList.summary, [])
|
||||
});
|
||||
|
||||
this.imageUrls.push(this.imageService.randomize(this.imageService.getReadingListCoverImage(this.readingList.id)));
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -29,12 +55,22 @@ export class EditReadingListModalComponent implements OnInit {
|
|||
|
||||
save() {
|
||||
if (this.reviewGroup.value.title.trim() === '') return;
|
||||
const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted};
|
||||
|
||||
this.readingListService.update(model).subscribe(() => {
|
||||
|
||||
const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted, coverImageLocked: this.coverImageLocked};
|
||||
|
||||
const apis = [this.readingListService.update(model)];
|
||||
|
||||
if (this.selectedCover !== '') {
|
||||
apis.push(this.uploadService.updateReadingListCoverImage(this.readingList.id, this.selectedCover))
|
||||
}
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.readingList.title = model.title;
|
||||
this.readingList.summary = model.summary;
|
||||
this.readingList.coverImageLocked = this.coverImageLocked;
|
||||
this.ngModal.close(this.readingList);
|
||||
this.toastr.success('Reading List updated');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,4 +85,16 @@ export class EditReadingListModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.coverImageIndex = index;
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageLocked = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,42 +3,48 @@
|
|||
<span *ngIf="actions.length > 0">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
||||
</span>
|
||||
{{readingList.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
<span class="badge bg-primary rounded-pill" attr.aria-label="{{items.length}} total items">{{items.length}}</span>
|
||||
{{readingList?.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle>{{items.length}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||
<div class="mb-3">
|
||||
<!-- Action row-->
|
||||
<div class="row g-0">
|
||||
<div class="col-auto me-2">
|
||||
<button class="btn btn-primary" title="Read" (click)="read()">
|
||||
<span>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Read</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList?.promoted && !this.isAdmin">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="readingListImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-auto me-2">
|
||||
<!-- Action row-->
|
||||
<button class="btn btn-primary" title="Read" (click)="read()">
|
||||
<span>
|
||||
<i class="fa fa-check"></i>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Read</span> <!-- IDEA: We can provide them the ability to read/continue like we do with a series -->
|
||||
</span>
|
||||
<span class="read-btn--text"> Remove Read</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="accessibilit-mode" [value]="accessibilityMode" (change)="accessibilityMode = !accessibilityMode">
|
||||
<label class="form-check-label" for="accessibilit-mode">Order Numbers</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList?.promoted && !this.isAdmin">
|
||||
<span>
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
<span class="read-btn--text"> Remove Read</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="accessibilit-mode" [value]="accessibilityMode" (change)="accessibilityMode = !accessibilityMode">
|
||||
<label class="form-check-label" for="accessibilit-mode">Order Numbers</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="items.length === 0">
|
||||
No chapters added
|
||||
|
@ -63,6 +69,8 @@
|
|||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
<br/>
|
||||
<a href="javascript:void(0);" (click)="readChapter(item)">Read</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ReadingListService } from 'src/app/_services/reading-list.service';
|
|||
import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/dragable-ordered-list.component';
|
||||
import { LibraryService } from '../../_services/library.service';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-list-detail',
|
||||
|
@ -38,6 +39,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
|
||||
libraryTypes: {[key: number]: LibraryType} = {};
|
||||
|
||||
readingListImage: string = '';
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
@ -45,7 +48,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
|
||||
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
|
||||
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService) {}
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const listId = this.route.snapshot.paramMap.get('id');
|
||||
|
@ -57,6 +60,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
|
||||
this.listId = parseInt(listId, 10);
|
||||
|
||||
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
|
||||
|
||||
this.libraryService.getLibraries().subscribe(libs => {
|
||||
|
||||
});
|
||||
|
@ -107,6 +112,15 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
readChapter(item: ReadingListItem) {
|
||||
let reader = 'manga';
|
||||
if (item.seriesFormat === MangaFormat.EPUB) {
|
||||
reader = 'book;'
|
||||
}
|
||||
const params = this.readerService.getQueryParamsObject(false, true, this.readingList.id);
|
||||
this.router.navigate(['library', item.libraryId, 'series', item.seriesId, 'book', item.chapterId], {queryParams: params});
|
||||
}
|
||||
|
||||
handleReadingListActionCallback(action: Action, readingList: ReadingList) {
|
||||
switch(action) {
|
||||
case Action.Delete:
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal
|
|||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
|
||||
|
||||
|
@ -31,7 +32,8 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||
CardsModule,
|
||||
PipeModule,
|
||||
SharedModule,
|
||||
SidenavModule
|
||||
SidenavModule,
|
||||
NgbNavModule
|
||||
],
|
||||
exports: [
|
||||
AddToListModalComponent,
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [supressLibraryLink]="true" [imageUrl]="imageService.placeholderImage" (clicked)="handleClick(item)"></app-card-item>
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [supressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
|
4
UI/Web/storage/admin.json
Normal file
4
UI/Web/storage/admin.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"cookies": [],
|
||||
"origins": []
|
||||
}
|
4
UI/Web/storage/user.json
Normal file
4
UI/Web/storage/user.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"cookies": [],
|
||||
"origins": []
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue