mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-04-01 17:25:27 +02:00
Revert "Merge pull request #4 from philomena-dev/master"
This reverts commit42d87937eb
, reversing changes made to2cd8830191
. merp
This commit is contained in:
parent
eb5344ee06
commit
d3a1b1f1c0
49 changed files with 6902 additions and 2559 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -59,6 +59,6 @@ npm-debug.log
|
||||||
/native/**/target
|
/native/**/target
|
||||||
/.cargo
|
/.cargo
|
||||||
|
|
||||||
# Vitest coverage
|
# Jest coverage
|
||||||
/assets/coverage
|
/assets/coverage
|
||||||
docker/app/Dockerfile
|
docker/app/Dockerfile
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
js/vendor/*
|
js/vendor/*
|
||||||
vite.config.ts
|
webpack.config.js
|
||||||
|
jest.config.js
|
||||||
|
|
|
@ -10,7 +10,7 @@ parserOptions:
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- '@typescript-eslint'
|
- '@typescript-eslint'
|
||||||
- vitest
|
- jest
|
||||||
|
|
||||||
globals:
|
globals:
|
||||||
ga: false
|
ga: false
|
||||||
|
@ -276,14 +276,12 @@ overrides:
|
||||||
'@typescript-eslint/no-extra-parens': 2
|
'@typescript-eslint/no-extra-parens': 2
|
||||||
no-shadow: 0
|
no-shadow: 0
|
||||||
'@typescript-eslint/no-shadow': 2
|
'@typescript-eslint/no-shadow': 2
|
||||||
# Unit Tests (also written in TypeScript)
|
# Jest Tests (also written in TypeScript)
|
||||||
# Disable rules that do not make sense in test files (e.g. testing for undefined input values should be allowed)
|
# Disable rules that do not make sense in test files (e.g. testing for undefined input values should be allowed)
|
||||||
- files:
|
- files:
|
||||||
- '*.spec.ts'
|
- '*.spec.ts'
|
||||||
- 'test/*.ts'
|
- 'test/*.ts'
|
||||||
extends:
|
extends:
|
||||||
- 'plugin:vitest/legacy-recommended'
|
- 'plugin:jest/recommended'
|
||||||
rules:
|
rules:
|
||||||
no-undefined: 0
|
no-undefined: 0
|
||||||
no-unused-expressions: 0
|
|
||||||
vitest/valid-expect: 0
|
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
@import "global";
|
@import "global";
|
||||||
|
|
||||||
// Because FA is a SPECIAL SNOWFLAKE.
|
// Because FA is a SPECIAL SNOWFLAKE.
|
||||||
$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
|
$fa-font-path: '~@fortawesome/fontawesome-free/webfonts';
|
||||||
|
|
||||||
@import "@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||||
@import "@fortawesome/fontawesome-free/scss/solid.scss";
|
@import "~@fortawesome/fontawesome-free/scss/solid.scss";
|
||||||
@import "@fortawesome/fontawesome-free/scss/regular.scss";
|
@import "~@fortawesome/fontawesome-free/scss/regular.scss";
|
||||||
@import "@fortawesome/fontawesome-free/scss/brands.scss";
|
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
|
||||||
@import "normalize-scss/sass/normalize/import-now";
|
@import "~normalize-scss/sass/normalize/import-now";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: $background_color;
|
background-color: $background_color;
|
||||||
|
@ -469,26 +469,26 @@ span.stat {
|
||||||
@import "shame";
|
@import "shame";
|
||||||
@import "text";
|
@import "text";
|
||||||
|
|
||||||
@import "views/adverts";
|
@import "~views/adverts";
|
||||||
@import "views/approval";
|
@import "~views/approval";
|
||||||
@import "views/badges";
|
@import "~views/badges";
|
||||||
@import "views/channels";
|
@import "~views/channels";
|
||||||
@import "views/comments";
|
@import "~views/comments";
|
||||||
@import "views/commissions";
|
@import "~views/commissions";
|
||||||
@import "views/communications";
|
@import "~views/communications";
|
||||||
@import "views/duplicate_reports";
|
@import "~views/duplicate_reports";
|
||||||
@import "views/filters";
|
@import "~views/filters";
|
||||||
@import "views/galleries";
|
@import "~views/galleries";
|
||||||
@import "views/images";
|
@import "~views/images";
|
||||||
@import "views/pages";
|
@import "~views/pages";
|
||||||
@import "views/polls";
|
@import "~views/polls";
|
||||||
@import "views/posts";
|
@import "~views/posts";
|
||||||
@import "views/profiles";
|
@import "~views/profiles";
|
||||||
@import "views/pagination";
|
@import "~views/pagination";
|
||||||
@import "views/search";
|
@import "~views/search";
|
||||||
@import "views/staff";
|
@import "~views/staff";
|
||||||
@import "views/stats";
|
@import "~views/stats";
|
||||||
@import "views/tags";
|
@import "~views/tags";
|
||||||
|
|
||||||
.no-overflow {
|
.no-overflow {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -124,8 +124,6 @@ a.block__header--single-item, .block__header a {
|
||||||
.block__header--js-tabbed {
|
.block__header--js-tabbed {
|
||||||
@extend .block__header--light;
|
@extend .block__header--light;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
border-bottom: $border;
|
border-bottom: $border;
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -23,11 +23,6 @@
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__navigation {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.header__link {
|
a.header__link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 $header_spacing;
|
padding: 0 $header_spacing;
|
||||||
|
|
|
@ -190,4 +190,4 @@ $dnp_warning_hover_color: lighten($vote_down_color, 10%);
|
||||||
$poll_form_label_background: lighten($border_color, 8);
|
$poll_form_label_background: lighten($border_color, 8);
|
||||||
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
||||||
|
|
||||||
@import "common/base";
|
@import "~common/base";
|
||||||
|
|
|
@ -180,4 +180,4 @@ $dnp_warning_hover_color: lighten($vote_down_color, 10%);
|
||||||
$poll_form_label_background: lighten($border_color, 8);
|
$poll_form_label_background: lighten($border_color, 8);
|
||||||
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
||||||
|
|
||||||
@import "common/base";
|
@import "~common/base";
|
||||||
|
|
|
@ -192,4 +192,4 @@ $dnp_warning_hover_color: lighten($vote_down_color, 10%);
|
||||||
$poll_form_label_background: lighten($border_color, 8);
|
$poll_form_label_background: lighten($border_color, 8);
|
||||||
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
$tag_dropdown_hover_background: darken($meta_color, 4%);
|
||||||
|
|
||||||
@import "common/base";
|
@import "~common/base";
|
||||||
|
|
|
@ -92,6 +92,12 @@ div.image-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* prevent .media-box__overlay from overflowing the container */
|
/* prevent .media-box__overlay from overflowing the container */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
a::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -99,12 +105,12 @@ div.image-container {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
/* Make the link cover the whole container if the image is oblong */
|
/* Make the link cover the whole container if the image is oblong */
|
||||||
a, picture, video {
|
a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
justify-content: center;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,11 +70,7 @@
|
||||||
.tag > span {
|
.tag > span {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
white-space: pre;
|
||||||
|
|
||||||
.tag-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag a {
|
.tag a {
|
||||||
|
|
13
assets/fix-jsdom.ts
Normal file
13
assets/fix-jsdom.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import JSDOMEnvironment from 'jest-environment-jsdom';
|
||||||
|
|
||||||
|
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
|
||||||
|
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
|
||||||
|
// jsdom URL and Blob are missing most of the implementation
|
||||||
|
// Use the node version of these types instead
|
||||||
|
this.global.URL = URL;
|
||||||
|
this.global.Blob = Blob;
|
||||||
|
}
|
||||||
|
}
|
42
assets/jest.config.js
Normal file
42
assets/jest.config.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
export default {
|
||||||
|
collectCoverage: true,
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'js/**/*.{js,ts}',
|
||||||
|
],
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
'/node_modules/',
|
||||||
|
'/.*\\.test\\.ts$',
|
||||||
|
'.*\\.d\\.ts$',
|
||||||
|
],
|
||||||
|
coverageDirectory: '<rootDir>/coverage/',
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
statements: 0,
|
||||||
|
branches: 0,
|
||||||
|
functions: 0,
|
||||||
|
lines: 0,
|
||||||
|
},
|
||||||
|
'./js/utils/**/*.ts': {
|
||||||
|
statements: 100,
|
||||||
|
branches: 100,
|
||||||
|
functions: 100,
|
||||||
|
lines: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preset: 'ts-jest/presets/js-with-ts-esm',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||||
|
testEnvironment: './fix-jsdom.ts',
|
||||||
|
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'./js/(.*)': '<rootDir>/js/$1',
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
|
tsconfig: '<rootDir>/tsconfig.json',
|
||||||
|
useESM: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
extensionsToTreatAsEsm: ['.ts', '.js'],
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,7 +1,6 @@
|
||||||
import { inputDuplicatorCreator } from '../input-duplicator';
|
import { inputDuplicatorCreator } from '../input-duplicator';
|
||||||
import { assertNotNull } from '../utils/assert';
|
import { assertNotNull } from '../utils/assert';
|
||||||
import { $, $$, removeEl } from '../utils/dom';
|
import { $, $$, removeEl } from '../utils/dom';
|
||||||
import { fireEvent } from '@testing-library/dom';
|
|
||||||
|
|
||||||
describe('Input duplicator functionality', () => {
|
describe('Input duplicator functionality', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -42,7 +41,7 @@ describe('Input duplicator functionality', () => {
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(1);
|
expect($$('input')).toHaveLength(1);
|
||||||
|
|
||||||
fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
|
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(2);
|
expect($$('input')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
@ -54,7 +53,7 @@ describe('Input duplicator functionality', () => {
|
||||||
form.insertAdjacentElement('afterbegin', buttonDiv);
|
form.insertAdjacentElement('afterbegin', buttonDiv);
|
||||||
runCreator();
|
runCreator();
|
||||||
|
|
||||||
fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
|
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(2);
|
expect($$('input')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
@ -63,7 +62,7 @@ describe('Input duplicator functionality', () => {
|
||||||
runCreator();
|
runCreator();
|
||||||
|
|
||||||
for (let i = 0; i < 5; i += 1) {
|
for (let i = 0; i < 5; i += 1) {
|
||||||
fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
|
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(3);
|
expect($$('input')).toHaveLength(3);
|
||||||
|
@ -72,8 +71,8 @@ describe('Input duplicator functionality', () => {
|
||||||
it('should remove duplicated input elements', () => {
|
it('should remove duplicated input elements', () => {
|
||||||
runCreator();
|
runCreator();
|
||||||
|
|
||||||
fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
|
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
|
||||||
fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
|
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(1);
|
expect($$('input')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
@ -81,10 +80,10 @@ describe('Input duplicator functionality', () => {
|
||||||
it('should not remove the last input element', () => {
|
it('should not remove the last input element', () => {
|
||||||
runCreator();
|
runCreator();
|
||||||
|
|
||||||
fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
|
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
|
||||||
fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
|
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
|
||||||
for (let i = 0; i < 5; i += 1) {
|
for (let i = 0; i < 5; i += 1) {
|
||||||
fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
|
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect($$('input')).toHaveLength(1);
|
expect($$('input')).toHaveLength(1);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import fetchMock from 'jest-fetch-mock';
|
||||||
import { fireEvent, waitFor } from '@testing-library/dom';
|
import { fireEvent, waitFor } from '@testing-library/dom';
|
||||||
import { assertType } from '../utils/assert';
|
import { assertType } from '../utils/assert';
|
||||||
import '../ujs';
|
import '../ujs';
|
||||||
import { fetchMock } from '../../test/fetch-mock';
|
|
||||||
|
|
||||||
const mockEndpoint = 'http://localhost/endpoint';
|
const mockEndpoint = 'http://localhost/endpoint';
|
||||||
const mockVerb = 'POST';
|
const mockVerb = 'POST';
|
||||||
|
@ -38,7 +38,7 @@ describe('Remote utilities', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.insertAdjacentElement('beforeend', a);
|
document.documentElement.insertAdjacentElement('beforeend', a);
|
||||||
fireEvent.click(a, { button: 0 });
|
a.click();
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
@ -88,7 +88,7 @@ describe('Remote utilities', () => {
|
||||||
a.dataset.method = mockVerb;
|
a.dataset.method = mockVerb;
|
||||||
|
|
||||||
document.documentElement.insertAdjacentElement('beforeend', a);
|
document.documentElement.insertAdjacentElement('beforeend', a);
|
||||||
fireEvent.click(a);
|
a.click();
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
@ -128,7 +128,7 @@ describe('Remote utilities', () => {
|
||||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||||
reload: {
|
reload: {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: vi.fn(),
|
value: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -155,7 +155,7 @@ describe('Remote utilities', () => {
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
const form = configureForm();
|
const form = configureForm();
|
||||||
form.method = mockVerb;
|
form.method = mockVerb;
|
||||||
fireEvent.submit(form);
|
form.submit();
|
||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ describe('Remote utilities', () => {
|
||||||
it('should submit a PUT request with put data-method specified', () => {
|
it('should submit a PUT request with put data-method specified', () => {
|
||||||
const form = configureForm();
|
const form = configureForm();
|
||||||
form.dataset.method = 'put';
|
form.dataset.method = 'put';
|
||||||
fireEvent.submit(form);
|
form.submit();
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
expect(fetch).toHaveBeenNthCalledWith(1, mockEndpoint, {
|
expect(fetch).toHaveBeenNthCalledWith(1, mockEndpoint, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
@ -201,7 +201,7 @@ describe('Remote utilities', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should reload the page on 300 multiple choices response', () => {
|
it('should reload the page on 300 multiple choices response', () => {
|
||||||
vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300}));
|
jest.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300}));
|
||||||
|
|
||||||
submitForm();
|
submitForm();
|
||||||
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
|
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
|
||||||
|
@ -211,29 +211,28 @@ describe('Remote utilities', () => {
|
||||||
|
|
||||||
describe('Form utilities', () => {
|
describe('Form utilities', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
||||||
cb(1);
|
cb(1);
|
||||||
return 1;
|
return 1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('[data-confirm]', () => {
|
describe('[data-confirm]', () => {
|
||||||
const createA = () => {
|
const createA = () => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.dataset.confirm = 'confirm';
|
a.dataset.confirm = 'confirm';
|
||||||
// We cannot use mockEndpoint here since anything except a hash change will log an error in the test output
|
a.href = mockEndpoint;
|
||||||
a.href = '#hash';
|
|
||||||
document.documentElement.insertAdjacentElement('beforeend', a);
|
document.documentElement.insertAdjacentElement('beforeend', a);
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should cancel the event on failed confirm', () => {
|
it('should cancel the event on failed confirm', () => {
|
||||||
const a = createA();
|
const a = createA();
|
||||||
const confirm = vi.spyOn(window, 'confirm').mockImplementationOnce(() => false);
|
const confirm = jest.spyOn(window, 'confirm').mockImplementationOnce(() => false);
|
||||||
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
|
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||||
|
|
||||||
expect(fireEvent(a, event)).toBe(false);
|
expect(fireEvent(a, event)).toBe(false);
|
||||||
|
@ -242,7 +241,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should allow the event on confirm', () => {
|
it('should allow the event on confirm', () => {
|
||||||
const a = createA();
|
const a = createA();
|
||||||
const confirm = vi.spyOn(window, 'confirm').mockImplementationOnce(() => true);
|
const confirm = jest.spyOn(window, 'confirm').mockImplementationOnce(() => true);
|
||||||
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
|
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||||
|
|
||||||
expect(fireEvent(a, event)).toBe(true);
|
expect(fireEvent(a, event)).toBe(true);
|
||||||
|
@ -277,7 +276,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should disable submit button containing a text child on click', () => {
|
it('should disable submit button containing a text child on click', () => {
|
||||||
const [ , button ] = createFormAndButton(submitText, loadingText);
|
const [ , button ] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
|
|
||||||
expect(button.textContent).toEqual(' Loading...');
|
expect(button.textContent).toEqual(' Loading...');
|
||||||
expect(button.dataset.enableWith).toEqual(submitText);
|
expect(button.dataset.enableWith).toEqual(submitText);
|
||||||
|
@ -285,7 +284,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should disable submit button containing element children on click', () => {
|
it('should disable submit button containing element children on click', () => {
|
||||||
const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
|
|
||||||
expect(button.innerHTML).toEqual(loadingMarkup);
|
expect(button.innerHTML).toEqual(loadingMarkup);
|
||||||
expect(button.dataset.enableWith).toEqual(submitMarkup);
|
expect(button.dataset.enableWith).toEqual(submitMarkup);
|
||||||
|
@ -294,7 +293,7 @@ describe('Form utilities', () => {
|
||||||
it('should not disable anything when the form is invalid', () => {
|
it('should not disable anything when the form is invalid', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
||||||
form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />');
|
form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />');
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
|
|
||||||
expect(button.textContent).toEqual(submitText);
|
expect(button.textContent).toEqual(submitText);
|
||||||
expect(button.dataset.enableWith).not.toBeDefined();
|
expect(button.dataset.enableWith).not.toBeDefined();
|
||||||
|
@ -302,7 +301,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should reset submit button containing a text child on completion', () => {
|
it('should reset submit button containing a text child on completion', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
||||||
|
|
||||||
expect(button.textContent?.trim()).toEqual(submitText);
|
expect(button.textContent?.trim()).toEqual(submitText);
|
||||||
|
@ -311,7 +310,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should reset submit button containing element children on completion', () => {
|
it('should reset submit button containing element children on completion', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
||||||
|
|
||||||
expect(button.innerHTML).toEqual(submitMarkup);
|
expect(button.innerHTML).toEqual(submitMarkup);
|
||||||
|
@ -320,7 +319,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
it('should reset disabled form elements on pageshow', () => {
|
it('should reset disabled form elements on pageshow', () => {
|
||||||
const [ , button ] = createFormAndButton(submitText, loadingText);
|
const [ , button ] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
button.click();
|
||||||
fireEvent(window, new CustomEvent('pageshow'));
|
fireEvent(window, new CustomEvent('pageshow'));
|
||||||
|
|
||||||
expect(button.textContent?.trim()).toEqual(submitText);
|
expect(button.textContent?.trim()).toEqual(submitText);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { $, $$, removeEl } from '../utils/dom';
|
import { $, $$, removeEl } from '../utils/dom';
|
||||||
import { assertNotNull, assertNotUndefined } from '../utils/assert';
|
import { assertNotNull, assertNotUndefined } from '../utils/assert';
|
||||||
|
|
||||||
import { fetchMock } from '../../test/fetch-mock';
|
import fetchMock from 'jest-fetch-mock';
|
||||||
import { fixEventListeners } from '../../test/fix-event-listeners';
|
import { fixEventListeners } from '../../test/fix-event-listeners';
|
||||||
import { fireEvent, waitFor } from '@testing-library/dom';
|
import { fireEvent, waitFor } from '@testing-library/dom';
|
||||||
import { promises } from 'fs';
|
import { promises } from 'fs';
|
||||||
|
@ -13,8 +13,8 @@ import { setupImageUpload } from '../upload';
|
||||||
const scrapeResponse = {
|
const scrapeResponse = {
|
||||||
description: 'test',
|
description: 'test',
|
||||||
images: [
|
images: [
|
||||||
{ url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1' },
|
{url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1'},
|
||||||
{ url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2' },
|
{url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2'},
|
||||||
],
|
],
|
||||||
source_url: 'http://localhost/images',
|
source_url: 'http://localhost/images',
|
||||||
author_name: 'test',
|
author_name: 'test',
|
||||||
|
@ -47,7 +47,6 @@ describe('Image upload form', () => {
|
||||||
|
|
||||||
fixEventListeners(window);
|
fixEventListeners(window);
|
||||||
|
|
||||||
|
|
||||||
let form: HTMLFormElement;
|
let form: HTMLFormElement;
|
||||||
let imgPreviews: HTMLDivElement;
|
let imgPreviews: HTMLDivElement;
|
||||||
let fileField: HTMLInputElement;
|
let fileField: HTMLInputElement;
|
||||||
|
@ -58,10 +57,6 @@ describe('Image upload form', () => {
|
||||||
let sourceEl: HTMLInputElement;
|
let sourceEl: HTMLInputElement;
|
||||||
let descrEl: HTMLTextAreaElement;
|
let descrEl: HTMLTextAreaElement;
|
||||||
|
|
||||||
const assertFetchButtonIsDisabled = () => {
|
|
||||||
if (!fetchButton.hasAttribute('disabled')) throw new Error('fetchButton is not disabled');
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.documentElement.insertAdjacentHTML('beforeend', `
|
document.documentElement.insertAdjacentHTML('beforeend', `
|
||||||
<form action="/images">
|
<form action="/images">
|
||||||
|
@ -96,37 +91,28 @@ describe('Image upload form', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable fetch button on empty source', () => {
|
it('should disable fetch button on empty source', () => {
|
||||||
fireEvent.input(remoteUrl, { target: { value: '' } });
|
fireEvent.input(remoteUrl, { target: { value: '' }});
|
||||||
expect(fetchButton.disabled).toBe(true);
|
expect(fetchButton.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable fetch button on non-empty source', () => {
|
it('should enable fetch button on non-empty source', () => {
|
||||||
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }});
|
||||||
expect(fetchButton.disabled).toBe(false);
|
expect(fetchButton.disabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a preview element when an image file is uploaded', () => {
|
it('should create a preview element when an image file is uploaded', () => {
|
||||||
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
fireEvent.change(fileField, { target: { files: [mockPng] }});
|
||||||
return waitFor(() => {
|
return waitFor(() => expect(imgPreviews.querySelectorAll('img')).toHaveLength(1));
|
||||||
assertFetchButtonIsDisabled();
|
|
||||||
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a preview element when a Matroska video file is uploaded', () => {
|
it('should create a preview element when a Matroska video file is uploaded', () => {
|
||||||
fireEvent.change(fileField, { target: { files: [mockWebm] } });
|
fireEvent.change(fileField, { target: { files: [mockWebm] }});
|
||||||
return waitFor(() => {
|
return waitFor(() => expect(imgPreviews.querySelectorAll('video')).toHaveLength(1));
|
||||||
assertFetchButtonIsDisabled();
|
|
||||||
expect(imgPreviews.querySelectorAll('video')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should block navigation away after an image file is attached, but not after form submission', async() => {
|
it('should block navigation away after an image file is attached, but not after form submission', async() => {
|
||||||
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
fireEvent.change(fileField, { target: { files: [mockPng] }});
|
||||||
await waitFor(() => {
|
await waitFor(() => { expect(imgPreviews.querySelectorAll('img')).toHaveLength(1); });
|
||||||
assertFetchButtonIsDisabled();
|
|
||||||
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const failedUnloadEvent = new Event('beforeunload', { cancelable: true });
|
const failedUnloadEvent = new Event('beforeunload', { cancelable: true });
|
||||||
expect(fireEvent(window, failedUnloadEvent)).toBe(false);
|
expect(fireEvent(window, failedUnloadEvent)).toBe(false);
|
||||||
|
@ -136,7 +122,7 @@ describe('Image upload form', () => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
fireEvent.submit(form);
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
|
const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
|
||||||
|
@ -145,11 +131,11 @@ describe('Image upload form', () => {
|
||||||
|
|
||||||
it('should scrape images when the fetch button is clicked', async() => {
|
it('should scrape images when the fetch button is clicked', async() => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
|
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
|
||||||
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }});
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
tagsEl.addEventListener('addtag', (event: Event) => {
|
tagsEl.addEventListener('addtag', (event: Event) => {
|
||||||
expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' });
|
expect((event as CustomEvent).detail).toEqual({name: 'artist:test'});
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -167,8 +153,8 @@ describe('Image upload form', () => {
|
||||||
it('should show null scrape result', () => {
|
it('should show null scrape result', () => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify(nullResponse), { status: 200 }));
|
fetchMock.mockResolvedValue(new Response(JSON.stringify(nullResponse), { status: 200 }));
|
||||||
|
|
||||||
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }});
|
||||||
fireEvent.click(fetchButton);
|
fetchButton.click();
|
||||||
|
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
@ -180,8 +166,8 @@ describe('Image upload form', () => {
|
||||||
it('should show error scrape result', () => {
|
it('should show error scrape result', () => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify(errorResponse), { status: 200 }));
|
fetchMock.mockResolvedValue(new Response(JSON.stringify(errorResponse), { status: 200 }));
|
||||||
|
|
||||||
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }});
|
||||||
fireEvent.click(fetchButton);
|
fetchButton.click();
|
||||||
|
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
@ -8,12 +8,3 @@
|
||||||
// Our code
|
// Our code
|
||||||
import './ujs';
|
import './ujs';
|
||||||
import './when-ready';
|
import './when-ready';
|
||||||
|
|
||||||
// When developing CSS, include the relevant CSS you're working on here
|
|
||||||
// in order to enable HMR (live reload) on it.
|
|
||||||
// Would typically be either the theme file, or any additional file
|
|
||||||
// you later intend to put in the <link> tag.
|
|
||||||
|
|
||||||
// import '../css/themes/default.scss';
|
|
||||||
// import '../css/themes/dark.scss';
|
|
||||||
// import '../css/themes/red.scss';
|
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
* Autocomplete.
|
* Autocomplete.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LocalAutocompleter } from './utils/local-autocompleter';
|
import { LocalAutocompleter } from 'utils/local-autocompleter';
|
||||||
import { handleError } from './utils/requests';
|
import { handleError } from 'utils/requests';
|
||||||
|
|
||||||
const cache = {};
|
const cache = {};
|
||||||
let inputField, originalTerm;
|
let inputField, originalTerm;
|
||||||
|
|
|
@ -29,11 +29,11 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
describe('$', () => {
|
describe('$', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the native querySelector method on document by default', () => {
|
it('should call the native querySelector method on document by default', () => {
|
||||||
const spy = vi.spyOn(document, 'querySelector');
|
const spy = jest.spyOn(document, 'querySelector');
|
||||||
|
|
||||||
mockSelectors.forEach((selector, nthCall) => {
|
mockSelectors.forEach((selector, nthCall) => {
|
||||||
$(selector);
|
$(selector);
|
||||||
|
@ -43,7 +43,7 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
it('should call the native querySelector method on the passed element', () => {
|
it('should call the native querySelector method on the passed element', () => {
|
||||||
const mockElement = document.createElement('br');
|
const mockElement = document.createElement('br');
|
||||||
const spy = vi.spyOn(mockElement, 'querySelector');
|
const spy = jest.spyOn(mockElement, 'querySelector');
|
||||||
|
|
||||||
mockSelectors.forEach((selector, nthCall) => {
|
mockSelectors.forEach((selector, nthCall) => {
|
||||||
// FIXME This will not be necessary once the file is properly typed
|
// FIXME This will not be necessary once the file is properly typed
|
||||||
|
@ -55,11 +55,11 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
describe('$$', () => {
|
describe('$$', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the native querySelectorAll method on document by default', () => {
|
it('should call the native querySelectorAll method on document by default', () => {
|
||||||
const spy = vi.spyOn(document, 'querySelectorAll');
|
const spy = jest.spyOn(document, 'querySelectorAll');
|
||||||
|
|
||||||
mockSelectors.forEach((selector, nthCall) => {
|
mockSelectors.forEach((selector, nthCall) => {
|
||||||
$$(selector);
|
$$(selector);
|
||||||
|
@ -69,7 +69,7 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
it('should call the native querySelectorAll method on the passed element', () => {
|
it('should call the native querySelectorAll method on the passed element', () => {
|
||||||
const mockElement = document.createElement('br');
|
const mockElement = document.createElement('br');
|
||||||
const spy = vi.spyOn(mockElement, 'querySelectorAll');
|
const spy = jest.spyOn(mockElement, 'querySelectorAll');
|
||||||
|
|
||||||
mockSelectors.forEach((selector, nthCall) => {
|
mockSelectors.forEach((selector, nthCall) => {
|
||||||
// FIXME This will not be necessary once the file is properly typed
|
// FIXME This will not be necessary once the file is properly typed
|
||||||
|
@ -311,7 +311,7 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
describe('removeEl', () => {
|
describe('removeEl', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT throw error if element has no parent', () => {
|
it('should NOT throw error if element has no parent', () => {
|
||||||
|
@ -324,7 +324,7 @@ describe('DOM Utilities', () => {
|
||||||
const childNode = document.createElement('p');
|
const childNode = document.createElement('p');
|
||||||
parentNode.appendChild(childNode);
|
parentNode.appendChild(childNode);
|
||||||
|
|
||||||
const spy = vi.spyOn(parentNode, 'removeChild');
|
const spy = jest.spyOn(parentNode, 'removeChild');
|
||||||
|
|
||||||
removeEl(childNode);
|
removeEl(childNode);
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
@ -374,7 +374,7 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call callback on left click', () => {
|
it('should call callback on left click', () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
cleanup = onLeftClick(mockCallback, element as unknown as Document);
|
cleanup = onLeftClick(mockCallback, element as unknown as Document);
|
||||||
|
|
||||||
|
@ -384,7 +384,7 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT call callback on non-left click', () => {
|
it('should NOT call callback on non-left click', () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
cleanup = onLeftClick(mockCallback, element as unknown as Document);
|
cleanup = onLeftClick(mockCallback, element as unknown as Document);
|
||||||
|
|
||||||
|
@ -395,7 +395,7 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add click event listener to the document by default', () => {
|
it('should add click event listener to the document by default', () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
cleanup = onLeftClick(mockCallback);
|
cleanup = onLeftClick(mockCallback);
|
||||||
|
|
||||||
fireEvent.click(document.body);
|
fireEvent.click(document.body);
|
||||||
|
@ -404,7 +404,7 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a cleanup function that removes the listener', () => {
|
it('should return a cleanup function that removes the listener', () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
const localCleanup = onLeftClick(mockCallback, element as unknown as Document);
|
const localCleanup = onLeftClick(mockCallback, element as unknown as Document);
|
||||||
|
|
||||||
|
@ -424,8 +424,8 @@ describe('DOM Utilities', () => {
|
||||||
describe('whenReady', () => {
|
describe('whenReady', () => {
|
||||||
it('should call callback immediately if document ready state is not loading', () => {
|
it('should call callback immediately if document ready state is not loading', () => {
|
||||||
const mockReadyStateValue = getRandomArrayItem<DocumentReadyState>(['complete', 'interactive']);
|
const mockReadyStateValue = getRandomArrayItem<DocumentReadyState>(['complete', 'interactive']);
|
||||||
const readyStateSpy = vi.spyOn(document, 'readyState', 'get').mockReturnValue(mockReadyStateValue);
|
const readyStateSpy = jest.spyOn(document, 'readyState', 'get').mockReturnValue(mockReadyStateValue);
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
whenReady(mockCallback);
|
whenReady(mockCallback);
|
||||||
|
@ -437,9 +437,9 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add event listener with callback if document ready state is loading', () => {
|
it('should add event listener with callback if document ready state is loading', () => {
|
||||||
const readyStateSpy = vi.spyOn(document, 'readyState', 'get').mockReturnValue('loading');
|
const readyStateSpy = jest.spyOn(document, 'readyState', 'get').mockReturnValue('loading');
|
||||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
whenReady(mockCallback);
|
whenReady(mockCallback);
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe('Draggable Utilities', () => {
|
||||||
const draggingClass = 'dragging';
|
const draggingClass = 'dragging';
|
||||||
const dragContainerClass = 'drag-container';
|
const dragContainerClass = 'drag-container';
|
||||||
const dragOverClass = 'over';
|
const dragOverClass = 'over';
|
||||||
let documentEventListenerSpy: MockInstance;
|
let documentEventListenerSpy: jest.SpyInstance;
|
||||||
|
|
||||||
let mockDragContainer: HTMLDivElement;
|
let mockDragContainer: HTMLDivElement;
|
||||||
let mockDraggable: HTMLDivElement;
|
let mockDraggable: HTMLDivElement;
|
||||||
|
@ -45,7 +45,7 @@ describe('Draggable Utilities', () => {
|
||||||
|
|
||||||
|
|
||||||
// Redirect all document event listeners to this element for easier cleanup
|
// Redirect all document event listeners to this element for easier cleanup
|
||||||
documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => {
|
documentEventListenerSpy = jest.spyOn(document, 'addEventListener').mockImplementation((...params) => {
|
||||||
mockDragContainer.addEventListener(...params);
|
mockDragContainer.addEventListener(...params);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -192,7 +192,7 @@ describe('Draggable Utilities', () => {
|
||||||
|
|
||||||
const mockDropEvent = createDragEvent('drop');
|
const mockDropEvent = createDragEvent('drop');
|
||||||
Object.assign(mockDropEvent, { clientX: 124 });
|
Object.assign(mockDropEvent, { clientX: 124 });
|
||||||
const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
const boundingBoxSpy = jest.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
||||||
left: 100,
|
left: 100,
|
||||||
width: 50,
|
width: 50,
|
||||||
} as unknown as DOMRect);
|
} as unknown as DOMRect);
|
||||||
|
@ -221,7 +221,7 @@ describe('Draggable Utilities', () => {
|
||||||
|
|
||||||
const mockDropEvent = createDragEvent('drop');
|
const mockDropEvent = createDragEvent('drop');
|
||||||
Object.assign(mockDropEvent, { clientX: 125 });
|
Object.assign(mockDropEvent, { clientX: 125 });
|
||||||
const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
const boundingBoxSpy = jest.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
|
||||||
left: 100,
|
left: 100,
|
||||||
width: 50,
|
width: 50,
|
||||||
} as unknown as DOMRect);
|
} as unknown as DOMRect);
|
||||||
|
@ -291,7 +291,7 @@ describe('Draggable Utilities', () => {
|
||||||
initDraggables();
|
initDraggables();
|
||||||
|
|
||||||
const mockEvent = createDragEvent('dragstart');
|
const mockEvent = createDragEvent('dragstart');
|
||||||
const draggableClosestSpy = vi.spyOn(mockDraggable, 'closest').mockReturnValue(null);
|
const draggableClosestSpy = jest.spyOn(mockDraggable, 'closest').mockReturnValue(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fireEvent(mockDraggable, mockEvent);
|
fireEvent(mockDraggable, mockEvent);
|
||||||
|
|
|
@ -8,7 +8,7 @@ describe('Event utils', () => {
|
||||||
describe('fire', () => {
|
describe('fire', () => {
|
||||||
it('should call the native dispatchEvent method on the element', () => {
|
it('should call the native dispatchEvent method on the element', () => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
const dispatchEventSpy = vi.spyOn(mockElement, 'dispatchEvent');
|
const dispatchEventSpy = jest.spyOn(mockElement, 'dispatchEvent');
|
||||||
const mockDetail = getRandomArrayItem([0, 'test', null]);
|
const mockDetail = getRandomArrayItem([0, 'test', null]);
|
||||||
|
|
||||||
fire(mockElement, mockEvent, mockDetail);
|
fire(mockElement, mockEvent, mockDetail);
|
||||||
|
@ -42,7 +42,7 @@ describe('Event utils', () => {
|
||||||
mockButton.classList.add('mock-button');
|
mockButton.classList.add('mock-button');
|
||||||
mockInnerElement.appendChild(mockButton);
|
mockInnerElement.appendChild(mockButton);
|
||||||
|
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = jest.fn();
|
||||||
on(mockElement, 'click', `.${innerClass}`, mockHandler);
|
on(mockElement, 'click', `.${innerClass}`, mockHandler);
|
||||||
|
|
||||||
fireEvent(mockButton, new Event('click', { bubbles: true }));
|
fireEvent(mockButton, new Event('click', { bubbles: true }));
|
||||||
|
@ -58,7 +58,7 @@ describe('Event utils', () => {
|
||||||
describe('leftClick', () => {
|
describe('leftClick', () => {
|
||||||
it('should fire on left click', () => {
|
it('should fire on left click', () => {
|
||||||
const mockButton = document.createElement('button');
|
const mockButton = document.createElement('button');
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = jest.fn();
|
||||||
|
|
||||||
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ describe('Event utils', () => {
|
||||||
|
|
||||||
it('should NOT fire on any other click', () => {
|
it('should NOT fire on any other click', () => {
|
||||||
const mockButton = document.createElement('button');
|
const mockButton = document.createElement('button');
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = jest.fn();
|
||||||
const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]);
|
const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
||||||
|
@ -83,7 +83,7 @@ describe('Event utils', () => {
|
||||||
describe('delegate', () => {
|
describe('delegate', () => {
|
||||||
it('should call the native addEventListener method on the element', () => {
|
it('should call the native addEventListener method on the element', () => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
const addEventListenerSpy = vi.spyOn(mockElement, 'addEventListener');
|
const addEventListenerSpy = jest.spyOn(mockElement, 'addEventListener');
|
||||||
|
|
||||||
delegate(mockElement, mockEvent, {});
|
delegate(mockElement, mockEvent, {});
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ describe('Event utils', () => {
|
||||||
const mockButton = document.createElement('button');
|
const mockButton = document.createElement('button');
|
||||||
mockElement.appendChild(mockButton);
|
mockElement.appendChild(mockButton);
|
||||||
|
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = jest.fn();
|
||||||
delegate(mockElement, 'click', { [`.${parentClass}`]: mockHandler });
|
delegate(mockElement, 'click', { [`.${parentClass}`]: mockHandler });
|
||||||
|
|
||||||
fireEvent(mockButton, new Event('click', { bubbles: true }));
|
fireEvent(mockButton, new Event('click', { bubbles: true }));
|
||||||
|
@ -127,8 +127,8 @@ describe('Event utils', () => {
|
||||||
const mockButton = document.createElement('button');
|
const mockButton = document.createElement('button');
|
||||||
mockWrapperElement.appendChild(mockButton);
|
mockWrapperElement.appendChild(mockButton);
|
||||||
|
|
||||||
const mockParentHandler = vi.fn();
|
const mockParentHandler = jest.fn();
|
||||||
const mockWrapperHandler = vi.fn().mockReturnValue(false);
|
const mockWrapperHandler = jest.fn().mockReturnValue(false);
|
||||||
delegate(mockElement, 'click', {
|
delegate(mockElement, 'click', {
|
||||||
[`.${wrapperClass}`]: mockWrapperHandler,
|
[`.${wrapperClass}`]: mockWrapperHandler,
|
||||||
[`.${parentClass}`]: mockParentHandler,
|
[`.${parentClass}`]: mockParentHandler,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { mockStorage } from '../../../test/mock-storage';
|
||||||
import { createEvent, fireEvent } from '@testing-library/dom';
|
import { createEvent, fireEvent } from '@testing-library/dom';
|
||||||
import { EventType } from '@testing-library/dom/types/events';
|
import { EventType } from '@testing-library/dom/types/events';
|
||||||
import { SpoilerType } from '../../../types/booru-object';
|
import { SpoilerType } from '../../../types/booru-object';
|
||||||
import { beforeEach } from 'vitest';
|
|
||||||
|
|
||||||
describe('Image utils', () => {
|
describe('Image utils', () => {
|
||||||
const hiddenClass = 'hidden';
|
const hiddenClass = 'hidden';
|
||||||
|
@ -83,10 +82,6 @@ describe('Image utils', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockServeHidpiValue = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('video thumbnail', () => {
|
describe('video thumbnail', () => {
|
||||||
type CreateMockElementsOptions = {
|
type CreateMockElementsOptions = {
|
||||||
extension: string;
|
extension: string;
|
||||||
|
@ -114,7 +109,7 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
mockElement.appendChild(mockVideo);
|
mockElement.appendChild(mockVideo);
|
||||||
const playSpy = vi.spyOn(mockVideo, 'play').mockReturnValue(Promise.resolve());
|
const playSpy = jest.spyOn(mockVideo, 'play').mockReturnValue(Promise.resolve());
|
||||||
|
|
||||||
const mockSpoilerOverlay = createMockSpoilerOverlay();
|
const mockSpoilerOverlay = createMockSpoilerOverlay();
|
||||||
mockElement.appendChild(mockSpoilerOverlay);
|
mockElement.appendChild(mockSpoilerOverlay);
|
||||||
|
@ -173,7 +168,7 @@ describe('Image utils', () => {
|
||||||
const { mockElement } = createMockElements({
|
const { mockElement } = createMockElements({
|
||||||
extension: 'webm',
|
extension: 'webm',
|
||||||
});
|
});
|
||||||
const jsonParseSpy = vi.spyOn(JSON, 'parse');
|
const jsonParseSpy = jest.spyOn(JSON, 'parse');
|
||||||
|
|
||||||
mockElement.removeAttribute(missingAttributeName);
|
mockElement.removeAttribute(missingAttributeName);
|
||||||
|
|
||||||
|
@ -387,7 +382,7 @@ describe('Image utils', () => {
|
||||||
it('should return early if picture AND video elements are missing', () => {
|
it('should return early if picture AND video elements are missing', () => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
|
|
||||||
const querySelectorSpy = vi.spyOn(mockElement, 'querySelector');
|
const querySelectorSpy = jest.spyOn(mockElement, 'querySelector');
|
||||||
|
|
||||||
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
||||||
|
|
||||||
|
@ -404,9 +399,9 @@ describe('Image utils', () => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
const mockVideo = document.createElement('video');
|
const mockVideo = document.createElement('video');
|
||||||
mockElement.appendChild(mockVideo);
|
mockElement.appendChild(mockVideo);
|
||||||
const pauseSpy = vi.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
|
const pauseSpy = jest.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
|
||||||
|
|
||||||
const querySelectorSpy = vi.spyOn(mockElement, 'querySelector');
|
const querySelectorSpy = jest.spyOn(mockElement, 'querySelector');
|
||||||
|
|
||||||
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
||||||
|
|
||||||
|
@ -428,7 +423,7 @@ describe('Image utils', () => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
const mockVideo = document.createElement('video');
|
const mockVideo = document.createElement('video');
|
||||||
mockElement.appendChild(mockVideo);
|
mockElement.appendChild(mockVideo);
|
||||||
const pauseSpy = vi.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
|
const pauseSpy = jest.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
|
||||||
const mockImage = document.createElement('img');
|
const mockImage = document.createElement('img');
|
||||||
mockImage.classList.add(hiddenClass);
|
mockImage.classList.add(hiddenClass);
|
||||||
mockElement.appendChild(mockImage);
|
mockElement.appendChild(mockImage);
|
||||||
|
@ -457,8 +452,8 @@ describe('Image utils', () => {
|
||||||
const mockPicture = document.createElement('picture');
|
const mockPicture = document.createElement('picture');
|
||||||
mockElement.appendChild(mockPicture);
|
mockElement.appendChild(mockPicture);
|
||||||
|
|
||||||
const imgQuerySelectorSpy = vi.spyOn(mockElement, 'querySelector');
|
const imgQuerySelectorSpy = jest.spyOn(mockElement, 'querySelector');
|
||||||
const pictureQuerySelectorSpy = vi.spyOn(mockPicture, 'querySelector');
|
const pictureQuerySelectorSpy = jest.spyOn(mockPicture, 'querySelector');
|
||||||
|
|
||||||
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
||||||
|
|
||||||
|
@ -498,7 +493,7 @@ describe('Image utils', () => {
|
||||||
describe('spoilerThumb', () => {
|
describe('spoilerThumb', () => {
|
||||||
const testSpoilerThumb = (handlers?: [EventType, EventType]) => {
|
const testSpoilerThumb = (handlers?: [EventType, EventType]) => {
|
||||||
const { mockElement, mockSpoilerOverlay, mockSizeImage } = createMockElementWithPicture('jpg');
|
const { mockElement, mockSpoilerOverlay, mockSizeImage } = createMockElementWithPicture('jpg');
|
||||||
const addEventListenerSpy = vi.spyOn(mockElement, 'addEventListener');
|
const addEventListenerSpy = jest.spyOn(mockElement, 'addEventListener');
|
||||||
|
|
||||||
spoilerThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
spoilerThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { fetchHtml, fetchJson, handleError } from '../requests';
|
import { fetchHtml, fetchJson, handleError } from '../requests';
|
||||||
import { fetchMock } from '../../../test/fetch-mock.ts';
|
import fetchMock from 'jest-fetch-mock';
|
||||||
|
|
||||||
describe('Request utils', () => {
|
describe('Request utils', () => {
|
||||||
const mockEndpoint = '/endpoint';
|
const mockEndpoint = '/endpoint';
|
||||||
|
|
|
@ -117,11 +117,11 @@ describe('Store utilities', () => {
|
||||||
it('should attach a storage event listener and fire when the provide key changes', () => {
|
it('should attach a storage event listener and fire when the provide key changes', () => {
|
||||||
const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`;
|
const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`;
|
||||||
const mockValue = Math.random();
|
const mockValue = Math.random();
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = jest.fn();
|
||||||
setStorageValue({
|
setStorageValue({
|
||||||
[mockKey]: JSON.stringify(mockValue),
|
[mockKey]: JSON.stringify(mockValue),
|
||||||
});
|
});
|
||||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||||
|
|
||||||
const cleanup = store.watch(mockKey, mockCallback);
|
const cleanup = store.watch(mockKey, mockCallback);
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ export function makeEl<Tag extends keyof HTMLElementTagNameMap>(tag: Tag, attr?:
|
||||||
if (attr) {
|
if (attr) {
|
||||||
for (const prop in attr) {
|
for (const prop in attr) {
|
||||||
const newValue = attr[prop];
|
const newValue = attr[prop];
|
||||||
if (newValue) {
|
if (typeof newValue !== 'undefined') {
|
||||||
el[prop] = newValue;
|
el[prop] = newValue as Exclude<typeof newValue, undefined>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8511
assets/package-lock.json
generated
8511
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,37 +1,60 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "cross-env NODE_ENV=production tsc && cross-env NODE_ENV=production vite build",
|
"deploy": "cross-env NODE_ENV=production webpack",
|
||||||
"lint": "eslint . --ext .js,.ts",
|
"lint": "eslint . --ext .js,.ts",
|
||||||
"test": "vitest run --coverage",
|
"test": "jest --ci",
|
||||||
"test:watch": "vitest watch --coverage",
|
"test:watch": "jest --watch",
|
||||||
"dev": "vite",
|
"watch": "webpack --watch"
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||||
"@types/web": "^0.0.143",
|
"@rollup/plugin-multi-entry": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
"@rollup/plugin-typescript": "^11.0.0",
|
||||||
"@typescript-eslint/parser": "^7.8.0",
|
"@rollup/plugin-virtual": "^3.0.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"@types/web": "^0.0.91",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
|
"acorn": "^8.8.2",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
|
"brunch": "^4.0.2",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"copycat-brunch": "^1.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^6.7.3",
|
||||||
|
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"eslint-webpack-plugin": "^4.0.0",
|
||||||
"normalize-scss": "^8.0.0",
|
"file-loader": "^6.2.0",
|
||||||
"sass": "^1.75.0",
|
"ignore-emit-webpack-plugin": "^2.0.6",
|
||||||
"typescript": "^5.4",
|
"jest-environment-jsdom": "^29.4.3",
|
||||||
"vite": "^5.2"
|
"mini-css-extract-plugin": "^2.7.2",
|
||||||
|
"normalize-scss": "^7.0.1",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"postcss-loader": "^7.2.4",
|
||||||
|
"postcss-scss": "^4.0.6",
|
||||||
|
"postcss-url": "^10.1.3",
|
||||||
|
"rollup": "^2.57.0",
|
||||||
|
"rollup-plugin-includepaths": "^0.2.4",
|
||||||
|
"sass": "^1.58.3",
|
||||||
|
"sass-loader": "^13.2.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"typescript": "^4.9",
|
||||||
|
"webpack": "^5.76.0",
|
||||||
|
"webpack-cli": "^5.0.1",
|
||||||
|
"webpack-rollup-loader": "^0.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.1.0",
|
"@testing-library/dom": "^9.0.0",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@types/chai-dom": "^1.11.3",
|
"@types/jest": "^29.4.0",
|
||||||
"@vitest/coverage-v8": "^1.5.3",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"chai": "^5",
|
"eslint-plugin-jest-dom": "^4.0.3",
|
||||||
"eslint-plugin-vitest": "^0.5.4",
|
"jest": "^29.4.3",
|
||||||
"jsdom": "^24.0.0",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"vitest": "^1.5.3",
|
"ts-jest": "^29.1.0"
|
||||||
"vitest-fetch-mock": "^0.2.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
|
||||||
import { vi } from 'vitest';
|
|
||||||
|
|
||||||
export const fetchMock = createFetchMock(vi);
|
|
21
assets/test/jest-setup.ts
Normal file
21
assets/test/jest-setup.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { matchNone } from '../js/query/boolean';
|
||||||
|
|
||||||
|
window.booru = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
timeAgo: () => {},
|
||||||
|
csrfToken: 'mockCsrfToken',
|
||||||
|
hiddenTag: '/mock-tagblocked.svg',
|
||||||
|
hiddenTagList: [],
|
||||||
|
ignoredTagList: [],
|
||||||
|
imagesWithDownvotingDisabled: [],
|
||||||
|
spoilerType: 'off',
|
||||||
|
spoileredTagList: [],
|
||||||
|
userCanEditFilter: false,
|
||||||
|
userIsSignedIn: false,
|
||||||
|
watchedTagList: [],
|
||||||
|
hiddenFilter: matchNone(),
|
||||||
|
spoileredFilter: matchNone(),
|
||||||
|
interactions: [],
|
||||||
|
tagsVersion: 5
|
||||||
|
};
|
|
@ -1,9 +1,9 @@
|
||||||
export function mockDateNow(initialDateNow: number): void {
|
export function mockDateNow(initialDateNow: number): void {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.useFakeTimers().setSystemTime(initialDateNow);
|
jest.useFakeTimers().setSystemTime(initialDateNow);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { MockInstance } from 'vitest';
|
|
||||||
|
|
||||||
type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem';
|
type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem';
|
||||||
|
|
||||||
export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage, Keys>): { [k in `${Keys}Spy`]: MockInstance } {
|
export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage, Keys>): { [k in `${Keys}Spy`]: jest.SpyInstance } {
|
||||||
const getItemSpy = 'getItem' in options ? vi.spyOn(Storage.prototype, 'getItem') : undefined;
|
const getItemSpy = 'getItem' in options ? jest.spyOn(Storage.prototype, 'getItem') : undefined;
|
||||||
const setItemSpy = 'setItem' in options ? vi.spyOn(Storage.prototype, 'setItem') : undefined;
|
const setItemSpy = 'setItem' in options ? jest.spyOn(Storage.prototype, 'setItem') : undefined;
|
||||||
const removeItemSpy = 'removeItem' in options ? vi.spyOn(Storage.prototype, 'removeItem') : undefined;
|
const removeItemSpy = 'removeItem' in options ? jest.spyOn(Storage.prototype, 'removeItem') : undefined;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
getItemSpy && getItemSpy.mockImplementation((options as Storage).getItem);
|
getItemSpy && getItemSpy.mockImplementation((options as Storage).getItem);
|
||||||
|
@ -28,7 +26,7 @@ export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage,
|
||||||
return { getItemSpy, setItemSpy, removeItemSpy } as ReturnType<typeof mockStorage>;
|
return { getItemSpy, setItemSpy, removeItemSpy } as ReturnType<typeof mockStorage>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: MockInstance } & {
|
type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: jest.SpyInstance } & {
|
||||||
/**
|
/**
|
||||||
* Forces the mock storage back to its default (empty) state
|
* Forces the mock storage back to its default (empty) state
|
||||||
* @param value
|
* @param value
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { matchNone } from '../js/query/boolean';
|
|
||||||
import '@testing-library/jest-dom/vitest';
|
|
||||||
import { URL } from 'node:url';
|
|
||||||
import { Blob } from 'node:buffer';
|
|
||||||
import { fireEvent } from '@testing-library/dom';
|
|
||||||
|
|
||||||
window.booru = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
timeAgo: () => {},
|
|
||||||
csrfToken: 'mockCsrfToken',
|
|
||||||
hiddenTag: '/mock-tagblocked.svg',
|
|
||||||
hiddenTagList: [],
|
|
||||||
ignoredTagList: [],
|
|
||||||
imagesWithDownvotingDisabled: [],
|
|
||||||
spoilerType: 'off',
|
|
||||||
spoileredTagList: [],
|
|
||||||
userCanEditFilter: false,
|
|
||||||
userIsSignedIn: false,
|
|
||||||
watchedTagList: [],
|
|
||||||
hiddenFilter: matchNone(),
|
|
||||||
spoileredFilter: matchNone(),
|
|
||||||
interactions: [],
|
|
||||||
tagsVersion: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
|
|
||||||
// jsdom URL and Blob are missing most of the implementation
|
|
||||||
// Use the node version of these types instead
|
|
||||||
Object.assign(globalThis, { URL, Blob });
|
|
||||||
|
|
||||||
// Prevents an error when calling `form.submit()` directly in
|
|
||||||
// the code that is being tested
|
|
||||||
HTMLFormElement.prototype.submit = function() {
|
|
||||||
fireEvent.submit(this);
|
|
||||||
};
|
|
|
@ -1,26 +1,16 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
"baseUrl": "./js",
|
"baseUrl": "./js",
|
||||||
"target": "ES2020",
|
"target": "ES2018",
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2020",
|
"ES2018",
|
||||||
"DOM",
|
"DOM"
|
||||||
"DOM.Iterable"
|
|
||||||
],
|
],
|
||||||
|
"strict": true
|
||||||
"moduleResolution": "Node",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
|
|
||||||
"strict": true,
|
|
||||||
|
|
||||||
"types": ["vitest/globals"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
/// <reference types="vitest" />
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import autoprefixer from 'autoprefixer';
|
|
||||||
import { defineConfig, UserConfig, ConfigEnv } from 'vite';
|
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|
||||||
const isDev = command !== 'build' && mode !== 'test';
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
// Terminate the watcher when Phoenix quits
|
|
||||||
// @see https://moroz.dev/blog/integrating-vite-js-with-phoenix-1-6
|
|
||||||
process.stdin.on('close', () => {
|
|
||||||
// eslint-disable-next-line no-process-exit
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeNames =
|
|
||||||
fs.readdirSync(path.resolve(__dirname, 'css/themes/')).map(name => {
|
|
||||||
const m = name.match(/([-a-z]+).scss/);
|
|
||||||
|
|
||||||
if (m) { return m[1]; }
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const themes = new Map();
|
|
||||||
|
|
||||||
for (const name of themeNames) {
|
|
||||||
themes.set(`css/${name}`, `./css/themes/${name}.scss`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
publicDir: 'static',
|
|
||||||
plugins: [],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
common: path.resolve(__dirname, 'css/common/'),
|
|
||||||
views: path.resolve(__dirname, 'css/views/')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
target: 'es2020',
|
|
||||||
outDir: path.resolve(__dirname, '../priv/static'),
|
|
||||||
emptyOutDir: false,
|
|
||||||
sourcemap: isDev,
|
|
||||||
manifest: false,
|
|
||||||
cssCodeSplit: true,
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
'js/app': './js/app.js',
|
|
||||||
...Object.fromEntries(themes)
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
entryFileNames: '[name].js',
|
|
||||||
chunkFileNames: '[name].js',
|
|
||||||
assetFileNames: '[name][extname]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
postcss: {
|
|
||||||
plugins: [autoprefixer]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
globals: true,
|
|
||||||
environment: 'jsdom',
|
|
||||||
// TODO Jest --randomize CLI flag equivalent, consider enabling in the future
|
|
||||||
// sequence: { shuffle: true },
|
|
||||||
setupFiles: './test/vitest-setup.ts',
|
|
||||||
coverage: {
|
|
||||||
reporter: ['text', 'html'],
|
|
||||||
include: ['js/**/*.{js,ts}'],
|
|
||||||
exclude: [
|
|
||||||
'node_modules/',
|
|
||||||
'.*\\.test\\.ts$',
|
|
||||||
'.*\\.d\\.ts$',
|
|
||||||
],
|
|
||||||
thresholds: {
|
|
||||||
statements: 0,
|
|
||||||
branches: 0,
|
|
||||||
functions: 0,
|
|
||||||
lines: 0,
|
|
||||||
'**/utils/**/*.ts': {
|
|
||||||
statements: 100,
|
|
||||||
branches: 100,
|
|
||||||
functions: 100,
|
|
||||||
lines: 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
156
assets/webpack.config.js
Normal file
156
assets/webpack.config.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import url from 'url';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||||
|
import CopyPlugin from 'copy-webpack-plugin';
|
||||||
|
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||||
|
import IgnoreEmitPlugin from 'ignore-emit-webpack-plugin';
|
||||||
|
import ESLintPlugin from 'eslint-webpack-plugin';
|
||||||
|
import autoprefixer from 'autoprefixer';
|
||||||
|
import rollupPluginIncludepaths from 'rollup-plugin-includepaths';
|
||||||
|
import rollupPluginMultiEntry from '@rollup/plugin-multi-entry';
|
||||||
|
import rollupPluginTypescript from '@rollup/plugin-typescript';
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const includePaths = rollupPluginIncludepaths();
|
||||||
|
const multiEntry = rollupPluginMultiEntry();
|
||||||
|
const typescript = rollupPluginTypescript();
|
||||||
|
|
||||||
|
let plugins = [
|
||||||
|
new IgnoreEmitPlugin(/css\/.*(?<!css)$/),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].css',
|
||||||
|
chunkFilename: '[id].css'
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: path.resolve(__dirname, 'static') },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
|
plugins = plugins.concat([
|
||||||
|
new ESLintPlugin({
|
||||||
|
extensions: ['js', 'ts'],
|
||||||
|
failOnError: true,
|
||||||
|
failOnWarning: isDevelopment
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
plugins = plugins.concat([
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
}),
|
||||||
|
new CssMinimizerPlugin(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeNames =
|
||||||
|
fs.readdirSync(path.resolve(__dirname, 'css/themes')).map(name =>
|
||||||
|
name.match(/([-a-z]+).scss/)[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
const themes = {};
|
||||||
|
for (const name of themeNames) {
|
||||||
|
themes[`css/${name}`] = `./css/themes/${name}.scss`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mode: isDevelopment ? 'development' : 'production',
|
||||||
|
entry: {
|
||||||
|
'js/app.js': './js/app.js',
|
||||||
|
...themes
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name]',
|
||||||
|
path: path.resolve(__dirname, '../priv/static'),
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: !isDevelopment,
|
||||||
|
providedExports: true,
|
||||||
|
usedExports: true,
|
||||||
|
concatenateModules: true,
|
||||||
|
},
|
||||||
|
devtool: isDevelopment ? 'inline-source-map' : undefined,
|
||||||
|
performance: { hints: false },
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
common: path.resolve(__dirname, 'css/common/'),
|
||||||
|
views: path.resolve(__dirname, 'css/views/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(ttf|eot|svg|woff2?)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]',
|
||||||
|
outputPath: './fonts',
|
||||||
|
publicPath: '../fonts',
|
||||||
|
},
|
||||||
|
dependency: { not: ['url'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /app\.js/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'webpack-rollup-loader',
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
includePaths,
|
||||||
|
multiEntry,
|
||||||
|
typescript,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: isDevelopment,
|
||||||
|
url: {
|
||||||
|
filter: (url, _resourcePath) => {
|
||||||
|
return !url.startsWith('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
sourceMaps: isDevelopment,
|
||||||
|
ident: 'postcss',
|
||||||
|
syntax: 'postcss-scss',
|
||||||
|
plugins: [
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: isDevelopment,
|
||||||
|
sassOptions: {
|
||||||
|
quietDeps: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ config :philomena, Philomena.Repo, show_sensitive_data_on_connection_error: true
|
||||||
#
|
#
|
||||||
# The watchers configuration can be used to run external
|
# The watchers configuration can be used to run external
|
||||||
# watchers to your application. For example, we use it
|
# watchers to your application. For example, we use it
|
||||||
# with vite to recompile .js and .css sources.
|
# with webpack to recompile .js and .css sources.
|
||||||
config :philomena, PhilomenaWeb.Endpoint,
|
config :philomena, PhilomenaWeb.Endpoint,
|
||||||
http: [port: 4000],
|
http: [port: 4000],
|
||||||
debug_errors: true,
|
debug_errors: true,
|
||||||
|
@ -16,23 +16,11 @@ config :philomena, PhilomenaWeb.Endpoint,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
node: [
|
node: [
|
||||||
"node_modules/vite/bin/vite.js",
|
"node_modules/webpack/bin/webpack.js",
|
||||||
"--mode",
|
|
||||||
"development",
|
|
||||||
"--host",
|
|
||||||
"0.0.0.0",
|
|
||||||
"--config",
|
|
||||||
"vite.config.ts",
|
|
||||||
cd: Path.expand("../assets", __DIR__)
|
|
||||||
],
|
|
||||||
node: [
|
|
||||||
"node_modules/vite/bin/vite.js",
|
|
||||||
"build",
|
|
||||||
"--mode",
|
"--mode",
|
||||||
"development",
|
"development",
|
||||||
"--watch",
|
"--watch",
|
||||||
"--config",
|
"--watch-options-stdin",
|
||||||
"vite.config.ts",
|
|
||||||
cd: Path.expand("../assets", __DIR__)
|
cd: Path.expand("../assets", __DIR__)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -137,22 +137,10 @@ if config_env() == :prod do
|
||||||
url: [host: System.fetch_env!("APP_HOSTNAME"), scheme: "https", port: 443],
|
url: [host: System.fetch_env!("APP_HOSTNAME"), scheme: "https", port: 443],
|
||||||
secret_key_base: System.fetch_env!("SECRET_KEY_BASE"),
|
secret_key_base: System.fetch_env!("SECRET_KEY_BASE"),
|
||||||
server: not is_nil(System.get_env("START_ENDPOINT"))
|
server: not is_nil(System.get_env("START_ENDPOINT"))
|
||||||
|
|
||||||
# Do not relax CSP in production
|
|
||||||
config :philomena, csp_relaxed: false
|
|
||||||
|
|
||||||
# Disable Vite HMR in prod
|
|
||||||
config :philomena, vite_reload: false
|
|
||||||
else
|
else
|
||||||
# Don't send email in development
|
# Don't send email in development
|
||||||
config :philomena, Philomena.Mailer, adapter: Bamboo.LocalAdapter
|
config :philomena, Philomena.Mailer, adapter: Bamboo.LocalAdapter
|
||||||
|
|
||||||
# Use this to debug slime templates
|
# Use this to debug slime templates
|
||||||
# config :slime, :keep_lines, true
|
# config :slime, :keep_lines, true
|
||||||
|
|
||||||
# Relax CSP rules in development and test servers
|
|
||||||
config :philomena, csp_relaxed: true
|
|
||||||
|
|
||||||
# Enable Vite HMR
|
|
||||||
config :philomena, vite_reload: true
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,8 +51,6 @@ services:
|
||||||
- postgres
|
- postgres
|
||||||
- elasticsearch
|
- elasticsearch
|
||||||
- redis
|
- redis
|
||||||
ports:
|
|
||||||
- '5173:5173'
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16.2-alpine
|
image: postgres:16.2-alpine
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM elixir:1.16.2-alpine
|
FROM elixir:1.16.1-alpine
|
||||||
|
|
||||||
ADD https://api.github.com/repos/philomena-dev/FFmpeg/git/refs/heads/release/6.1 /tmp/ffmpeg_version.json
|
ADD https://api.github.com/repos/philomena-dev/FFmpeg/git/refs/heads/release/6.1 /tmp/ffmpeg_version.json
|
||||||
RUN (echo "https://github.com/philomena-dev/prebuilt-ffmpeg/raw/master"; cat /etc/apk/repositories) > /tmp/repositories \
|
RUN (echo "https://github.com/philomena-dev/prebuilt-ffmpeg/raw/master"; cat /etc/apk/repositories) > /tmp/repositories \
|
||||||
|
@ -24,5 +24,4 @@ COPY docker/app/run-test /usr/local/bin/run-test
|
||||||
COPY docker/app/safe-rsvg-convert /usr/local/bin/safe-rsvg-convert
|
COPY docker/app/safe-rsvg-convert /usr/local/bin/safe-rsvg-convert
|
||||||
COPY docker/app/purge-cache /usr/local/bin/purge-cache
|
COPY docker/app/purge-cache /usr/local/bin/purge-cache
|
||||||
ENV PATH=$PATH:/root/.cargo/bin
|
ENV PATH=$PATH:/root/.cargo/bin
|
||||||
EXPOSE 5173
|
|
||||||
CMD run-development
|
CMD run-development
|
||||||
|
|
|
@ -3,7 +3,6 @@ defmodule PhilomenaWeb.Profile.TagChangeController do
|
||||||
|
|
||||||
alias Philomena.Users.User
|
alias Philomena.Users.User
|
||||||
alias Philomena.Images.Image
|
alias Philomena.Images.Image
|
||||||
alias Philomena.Tags.Tag
|
|
||||||
alias Philomena.TagChanges.TagChange
|
alias Philomena.TagChanges.TagChange
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
@ -17,27 +16,19 @@ defmodule PhilomenaWeb.Profile.TagChangeController do
|
||||||
tag_changes =
|
tag_changes =
|
||||||
TagChange
|
TagChange
|
||||||
|> join(:inner, [tc], i in Image, on: tc.image_id == i.id)
|
|> join(:inner, [tc], i in Image, on: tc.image_id == i.id)
|
||||||
|> only_tag_join(params)
|
|
||||||
|> where(
|
|> where(
|
||||||
[tc, i],
|
[tc, i],
|
||||||
tc.user_id == ^user.id and not (i.user_id == ^user.id and i.anonymous == true)
|
tc.user_id == ^user.id and not (i.user_id == ^user.id and i.anonymous == true)
|
||||||
)
|
)
|
||||||
|> added_filter(params)
|
|> added_filter(params)
|
||||||
|> only_tag_filter(params)
|
|
||||||
|> preload([:tag, :user, image: [:user, :sources, tags: :aliases]])
|
|> preload([:tag, :user, image: [:user, :sources, tags: :aliases]])
|
||||||
|> order_by(desc: :id)
|
|> order_by(desc: :id)
|
||||||
|> Repo.paginate(conn.assigns.scrivener)
|
|> Repo.paginate(conn.assigns.scrivener)
|
||||||
|
|
||||||
# params.permit(:added, :only_tag) ...
|
|
||||||
pagination_params =
|
|
||||||
[added: conn.params["added"], only_tag: conn.params["only_tag"]]
|
|
||||||
|> Keyword.filter(fn {_k, v} -> not is_nil(v) and v != "" end)
|
|
||||||
|
|
||||||
render(conn, "index.html",
|
render(conn, "index.html",
|
||||||
title: "Tag Changes for User `#{user.name}'",
|
title: "Tag Changes for User `#{user.name}'",
|
||||||
user: user,
|
user: user,
|
||||||
tag_changes: tag_changes,
|
tag_changes: tag_changes
|
||||||
pagination_params: pagination_params
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -49,18 +40,4 @@ defmodule PhilomenaWeb.Profile.TagChangeController do
|
||||||
|
|
||||||
defp added_filter(query, _params),
|
defp added_filter(query, _params),
|
||||||
do: query
|
do: query
|
||||||
|
|
||||||
defp only_tag_join(query, %{"only_tag" => only_tag})
|
|
||||||
when is_binary(only_tag) and only_tag != "",
|
|
||||||
do: join(query, :inner, [tc], t in Tag, on: tc.tag_id == t.id)
|
|
||||||
|
|
||||||
defp only_tag_join(query, _params),
|
|
||||||
do: query
|
|
||||||
|
|
||||||
defp only_tag_filter(query, %{"only_tag" => only_tag})
|
|
||||||
when is_binary(only_tag) and only_tag != "",
|
|
||||||
do: where(query, [_, _, t], t.name == ^only_tag)
|
|
||||||
|
|
||||||
defp only_tag_filter(query, _params),
|
|
||||||
do: query
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
|
||||||
alias Philomena.Images.Image
|
alias Philomena.Images.Image
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
alias PhilomenaWeb.ImageView
|
alias PhilomenaWeb.ImageView
|
||||||
|
import Phoenix.HTML
|
||||||
import Phoenix.HTML.Link
|
import Phoenix.HTML.Link
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
@ -83,6 +84,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
|
||||||
size: ImageView.select_version(img, :medium),
|
size: ImageView.select_version(img, :medium),
|
||||||
conn: conn
|
conn: conn
|
||||||
)
|
)
|
||||||
|
|> safe_to_string()
|
||||||
|
|
||||||
[_id, "t"] when not img.hidden_from_users and img.approved ->
|
[_id, "t"] when not img.hidden_from_users and img.approved ->
|
||||||
Phoenix.View.render(ImageView, "_image_target.html",
|
Phoenix.View.render(ImageView, "_image_target.html",
|
||||||
|
@ -91,6 +93,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
|
||||||
size: ImageView.select_version(img, :small),
|
size: ImageView.select_version(img, :small),
|
||||||
conn: conn
|
conn: conn
|
||||||
)
|
)
|
||||||
|
|> safe_to_string()
|
||||||
|
|
||||||
[_id, "s"] when not img.hidden_from_users and img.approved ->
|
[_id, "s"] when not img.hidden_from_users and img.approved ->
|
||||||
Phoenix.View.render(ImageView, "_image_target.html",
|
Phoenix.View.render(ImageView, "_image_target.html",
|
||||||
|
@ -99,15 +102,18 @@ defmodule PhilomenaWeb.MarkdownRenderer do
|
||||||
size: ImageView.select_version(img, :thumb_small),
|
size: ImageView.select_version(img, :thumb_small),
|
||||||
conn: conn
|
conn: conn
|
||||||
)
|
)
|
||||||
|
|> safe_to_string()
|
||||||
|
|
||||||
[_id, suffix] when not img.approved ->
|
[_id, suffix] when not img.approved ->
|
||||||
">>#{img.id}#{suffix}#{link_suffix(img)}"
|
">>#{img.id}#{suffix}#{link_suffix(img)}"
|
||||||
|
|
||||||
[_id, ""] ->
|
[_id, ""] ->
|
||||||
link(">>#{img.id}#{link_suffix(img)}", to: "/images/#{img.id}")
|
link(">>#{img.id}#{link_suffix(img)}", to: "/images/#{img.id}")
|
||||||
|
|> safe_to_string()
|
||||||
|
|
||||||
[_id, suffix] when suffix in ["t", "s", "p"] ->
|
[_id, suffix] when suffix in ["t", "s", "p"] ->
|
||||||
link(">>#{img.id}#{suffix}#{link_suffix(img)}", to: "/images/#{img.id}")
|
link(">>#{img.id}#{suffix}#{link_suffix(img)}", to: "/images/#{img.id}")
|
||||||
|
|> safe_to_string()
|
||||||
|
|
||||||
# This condition should never trigger, but let's leave it here just in case.
|
# This condition should never trigger, but let's leave it here just in case.
|
||||||
[id, suffix] ->
|
[id, suffix] ->
|
||||||
|
@ -118,12 +124,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
|
||||||
">>#{text}"
|
">>#{text}"
|
||||||
end
|
end
|
||||||
|
|
||||||
string_contents =
|
[text, rendered]
|
||||||
rendered
|
|
||||||
|> Phoenix.HTML.Safe.to_iodata()
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|
|
||||||
[text, string_contents]
|
|
||||||
end)
|
end)
|
||||||
|> Map.new(fn [id, html] -> {id, html} end)
|
|> Map.new(fn [id, html] -> {id, html} end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,9 +24,8 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do
|
||||||
|
|
||||||
csp_config = [
|
csp_config = [
|
||||||
{:default_src, ["'self'"]},
|
{:default_src, ["'self'"]},
|
||||||
{:script_src, [default_script_src() | script_src]},
|
{:script_src, ["'self'" | script_src]},
|
||||||
{:connect_src, [default_connect_src()]},
|
{:style_src, ["'self'" | style_src]},
|
||||||
{:style_src, [default_style_src() | style_src]},
|
|
||||||
{:object_src, ["'none'"]},
|
{:object_src, ["'none'"]},
|
||||||
{:frame_ancestors, ["'none'"]},
|
{:frame_ancestors, ["'none'"]},
|
||||||
{:frame_src, frame_src || ["'none'"]},
|
{:frame_src, frame_src || ["'none'"]},
|
||||||
|
@ -42,13 +41,7 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do
|
||||||
|> Enum.map(&cspify_element/1)
|
|> Enum.map(&cspify_element/1)
|
||||||
|> Enum.join("; ")
|
|> Enum.join("; ")
|
||||||
|
|
||||||
if conn.status == 500 and allow_relaxed_csp() do
|
|
||||||
# Allow Plug.Debugger to function in this case
|
|
||||||
delete_resp_header(conn, "content-security-policy")
|
|
||||||
else
|
|
||||||
# Enforce CSP otherwise
|
|
||||||
put_resp_header(conn, "content-security-policy", csp_value)
|
put_resp_header(conn, "content-security-policy", csp_value)
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,14 +57,6 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do
|
||||||
|
|
||||||
defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri()
|
defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri()
|
||||||
defp camo_uri, do: Application.get_env(:philomena, :camo_host) |> to_uri()
|
defp camo_uri, do: Application.get_env(:philomena, :camo_host) |> to_uri()
|
||||||
defp vite_reload?, do: Application.get_env(:philomena, :vite_reload)
|
|
||||||
|
|
||||||
defp default_script_src, do: if(vite_reload?(), do: "'self' localhost:5173", else: "'self'")
|
|
||||||
|
|
||||||
defp default_connect_src,
|
|
||||||
do: if(vite_reload?(), do: "'self' localhost:5173 ws://localhost:5173", else: "'self'")
|
|
||||||
|
|
||||||
defp default_style_src, do: if(vite_reload?(), do: "'self' 'unsafe-inline'", else: "'self'")
|
|
||||||
|
|
||||||
defp to_uri(host) when host in [nil, ""], do: ""
|
defp to_uri(host) when host in [nil, ""], do: ""
|
||||||
defp to_uri(host), do: URI.to_string(%URI{scheme: "https", host: host})
|
defp to_uri(host), do: URI.to_string(%URI{scheme: "https", host: host})
|
||||||
|
@ -84,6 +69,4 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do
|
||||||
|
|
||||||
Enum.join([key | value], " ")
|
Enum.join([key | value], " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp allow_relaxed_csp, do: Application.get_env(:philomena, :csp_relaxed, false)
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,15 +45,13 @@ defmodule PhilomenaWeb.StatsUpdater do
|
||||||
distinct_creators: distinct_creators,
|
distinct_creators: distinct_creators,
|
||||||
images_in_galleries: images_in_galleries
|
images_in_galleries: images_in_galleries
|
||||||
)
|
)
|
||||||
|> Phoenix.HTML.Safe.to_iodata()
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|
|
||||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||||
|
|
||||||
static_page = %{
|
static_page = %{
|
||||||
title: "Statistics",
|
title: "Statistics",
|
||||||
slug: "stats",
|
slug: "stats",
|
||||||
body: result,
|
body: Phoenix.HTML.safe_to_string(result),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now
|
updated_at: now
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,6 @@ p
|
||||||
strong> Q: Do you host streams?
|
strong> Q: Do you host streams?
|
||||||
| A: No, we cheat and just link to streams on Picarto since that's where (almost) everyone is already. This is simply a nice way to track streaming artists.
|
| A: No, we cheat and just link to streams on Picarto since that's where (almost) everyone is already. This is simply a nice way to track streaming artists.
|
||||||
p
|
p
|
||||||
strong> Q: How do I get my stream/a friend's stream/<artist>'s stream here?
|
strong> Q: How do I get my stream/a friend's stream/<artist>'s stream here?
|
||||||
' A: Send a private message to a site administrator
|
' A: Send a private message to a site administrator
|
||||||
' with a link to the stream and the artist tag if applicable.
|
' with a link to the stream and the artist tag if applicable.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.hide-mobile.header__navigation
|
.hide-mobile
|
||||||
.dropdown.header__dropdown
|
.dropdown.header__dropdown
|
||||||
a.header__link href="/images"
|
a.header__link href="/images"
|
||||||
| Images
|
| Images
|
||||||
|
|
|
@ -18,14 +18,9 @@ html lang="en"
|
||||||
meta name="theme-color" content="#618fc3"
|
meta name="theme-color" content="#618fc3"
|
||||||
meta name="format-detection" content="telephone=no"
|
meta name="format-detection" content="telephone=no"
|
||||||
= csrf_meta_tag()
|
= csrf_meta_tag()
|
||||||
|
|
||||||
= if vite_reload?() do
|
|
||||||
script type="module" src="http://localhost:5173/@vite/client"
|
|
||||||
script type="module" src="http://localhost:5173/js/app.js"
|
|
||||||
- else
|
|
||||||
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
|
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
|
||||||
= render PhilomenaWeb.LayoutView, "_opengraph.html", assigns
|
= render PhilomenaWeb.LayoutView, "_opengraph.html", assigns
|
||||||
body data-theme=theme_name(@current_user) data-vite-reload=to_string(vite_reload?())
|
body data-theme=theme_name(@current_user)
|
||||||
= render PhilomenaWeb.LayoutView, "_burger.html", assigns
|
= render PhilomenaWeb.LayoutView, "_burger.html", assigns
|
||||||
#container class=container_class(@current_user)
|
#container class=container_class(@current_user)
|
||||||
= render PhilomenaWeb.LayoutView, "_header.html", assigns
|
= render PhilomenaWeb.LayoutView, "_header.html", assigns
|
||||||
|
|
|
@ -4,16 +4,16 @@ h1
|
||||||
= @user.name
|
= @user.name
|
||||||
|
|
||||||
- route = fn p -> Routes.profile_tag_change_path(@conn, :index, @user, p) end
|
- route = fn p -> Routes.profile_tag_change_path(@conn, :index, @user, p) end
|
||||||
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @tag_changes, route: route, conn: @conn, params: @pagination_params
|
- params = if @conn.params["added"], do: [added: @conn.params["added"]]
|
||||||
|
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @tag_changes, route: route, conn: @conn, params: params
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header
|
.block__header
|
||||||
= form_for @conn, Routes.profile_tag_change_path(@conn, :index, @user), [method: "get", enforce_utf8: false], fn f ->
|
span.block__header__title
|
||||||
= text_input f, :only_tag, class: "input", placeholder: "Tag", title: "Only show this tag", autocapitalize: "none"
|
| Display only:
|
||||||
= submit "Search", class: "button", title: "Search"
|
|
||||||
|
|
||||||
= link "Removed", to: Routes.profile_tag_change_path(@conn, :index, @user, Keyword.merge(@pagination_params, added: 0))
|
= link "Removed", to: Routes.profile_tag_change_path(@conn, :index, @user, added: 0)
|
||||||
= link "Added", to: Routes.profile_tag_change_path(@conn, :index, @user, Keyword.merge(@pagination_params, added: 1))
|
= link "Added", to: Routes.profile_tag_change_path(@conn, :index, @user, added: 1)
|
||||||
= link "All", to: Routes.profile_tag_change_path(@conn, :index, @user, Keyword.delete(@pagination_params, :added))
|
= link "All", to: Routes.profile_tag_change_path(@conn, :index, @user)
|
||||||
|
|
||||||
= render PhilomenaWeb.TagChangeView, "index.html", conn: @conn, tag_changes: @tag_changes, pagination: pagination
|
= render PhilomenaWeb.TagChangeView, "index.html", conn: @conn, tag_changes: @tag_changes, pagination: pagination
|
||||||
|
|
|
@ -22,10 +22,6 @@ defmodule PhilomenaWeb.LayoutView do
|
||||||
Application.get_env(:philomena, :cdn_host)
|
Application.get_env(:philomena, :cdn_host)
|
||||||
end
|
end
|
||||||
|
|
||||||
def vite_reload? do
|
|
||||||
Application.get_env(:philomena, :vite_reload)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ignored_tag_list(nil), do: []
|
defp ignored_tag_list(nil), do: []
|
||||||
defp ignored_tag_list([]), do: []
|
defp ignored_tag_list([]), do: []
|
||||||
defp ignored_tag_list([{tag, _body, _dnp_entries}]), do: [tag.id]
|
defp ignored_tag_list([{tag, _body, _dnp_entries}]), do: [tag.id]
|
||||||
|
|
|
@ -103,8 +103,6 @@ defmodule PhilomenaWeb.TagView do
|
||||||
{tags, shipping, data}
|
{tags, shipping, data}
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is a rendered template, so raw/1 has no effect on safety
|
|
||||||
# sobelow_skip ["XSS.Raw"]
|
|
||||||
defp render_quick_tags({tags, shipping, data}, conn) do
|
defp render_quick_tags({tags, shipping, data}, conn) do
|
||||||
render(PhilomenaWeb.TagView, "_quick_tag_table.html",
|
render(PhilomenaWeb.TagView, "_quick_tag_table.html",
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
@ -112,8 +110,6 @@ defmodule PhilomenaWeb.TagView do
|
||||||
data: data,
|
data: data,
|
||||||
conn: conn
|
conn: conn
|
||||||
)
|
)
|
||||||
|> Phoenix.HTML.Safe.to_iodata()
|
|
||||||
|> Phoenix.HTML.raw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp names_in_tab("default", data) do
|
defp names_in_tab("default", data) do
|
||||||
|
|
Loading…
Add table
Reference in a new issue