yt-grabber 1.8.7 → 1.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +29 -29
- package/.github/workflows/tests.yml +50 -0
- package/.vscode/tasks.json +13 -0
- package/README.md +3 -0
- package/eslint.config.ts +58 -3
- package/jest.config.ts +18 -2
- package/jest.setup.ts +10 -24
- package/package.json +2 -1
- package/src/automations/Helpers.test.ts +65 -0
- package/src/automations/Youtube.test.ts +232 -0
- package/src/automations/Youtube.ts +4 -6
- package/src/automations/YoutubeAlbums.test.ts +194 -0
- package/src/automations/YoutubeArtists.test.ts +231 -0
- package/src/automations/YoutubeArtists.ts +3 -3
- package/src/automations/YoutubeTracks.test.ts +195 -0
- package/src/bootstrap.test.tsx +56 -0
- package/src/common/CancellablePromise.test.ts +31 -0
- package/src/common/Delay.test.ts +32 -0
- package/src/common/FileSystem.test.ts +101 -0
- package/src/common/Formatters.test.ts +130 -0
- package/src/common/Helpers.test.ts +127 -0
- package/src/common/Helpers.ts +36 -0
- package/src/common/Logger.test.ts +74 -0
- package/src/common/Logger.ts +1 -1
- package/src/common/Promise.test.ts +53 -0
- package/src/common/PuppeteerOptions.test.ts +38 -0
- package/src/common/Reporter.test.ts +64 -0
- package/src/common/TestHelpers.ts +241 -0
- package/src/common/YtdplUtils.test.ts +280 -0
- package/src/common/YtdplUtils.ts +1 -1
- package/src/components/appBar/AppBar.test.tsx +106 -0
- package/src/components/fileField/FileField.test.tsx +93 -0
- package/src/components/languagePicker/LanguagePicker.test.tsx +67 -0
- package/src/components/languagePicker/LanguagePicker.tsx +7 -7
- package/src/components/modals/cutModal/CutModal.styl +21 -0
- package/src/components/modals/cutModal/CutModal.test.tsx +86 -0
- package/src/components/modals/cutModal/CutModal.tsx +203 -0
- package/src/components/modals/detailsModal/DetailsModal.test.tsx +55 -0
- package/src/components/modals/imageModal/ImageModal.test.tsx +37 -0
- package/src/components/modals/imageModal/ImageModal.tsx +1 -6
- package/src/components/modals/selectArtistModal/SelectArtistModal.test.tsx +56 -0
- package/src/components/numberField/NumberField.test.tsx +177 -0
- package/src/components/numberField/NumberField.tsx +1 -0
- package/src/components/themePicker/ThemePicker.test.tsx +51 -0
- package/src/components/themePicker/ThemePicker.tsx +3 -3
- package/src/components/youtube/formatSelector/FormatSelector.test.tsx +81 -0
- package/src/components/youtube/formatSelector/FormatSelector.tsx +3 -2
- package/src/components/youtube/infoBar/InfoBar.test.tsx +69 -0
- package/src/components/youtube/infoBar/InfoBar.tsx +79 -15
- package/src/components/youtube/inputModePicker/InputModePicker.test.tsx +95 -0
- package/src/components/youtube/inputModePicker/InputModePicker.tsx +8 -7
- package/src/components/youtube/inputPanel/InputPanel.test.tsx +176 -0
- package/src/components/youtube/inputPanel/InputPanel.tsx +17 -6
- package/src/components/youtube/logMenu/LogMenu.test.tsx +70 -0
- package/src/components/youtube/logMenu/LogMenu.tsx +2 -3
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +5 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.test.tsx +249 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +75 -9
- package/src/components/youtube/playlistTabs/PlaylistTabs.test.tsx +180 -0
- package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +1 -0
- package/src/components/youtube/trackList/TrackList.test.tsx +143 -0
- package/src/components/youtube/trackList/TrackList.tsx +3 -23
- package/src/hooks/useCancellablePromises.test.ts +58 -0
- package/src/hooks/useClickCounter.test.ts +60 -0
- package/src/hooks/useClickCounter.ts +1 -1
- package/src/hooks/useHelp.test.ts +109 -0
- package/src/hooks/useMultiClickHandler.test.ts +133 -0
- package/src/hooks/useMultiClickHandler.ts +1 -1
- package/src/hooks/useWindowUpdater.test.ts +47 -0
- package/src/i18next.test.ts +2 -2
- package/src/index.ts +1 -1
- package/src/messaging/MessageBus.test.ts +27 -0
- package/src/messaging/MessageChannel.test.ts +137 -0
- package/src/messaging/MessagingService.test.ts +64 -0
- package/src/messaging/MessagingService.ts +3 -0
- package/src/messaging/MultiMessageChannel.test.ts +56 -0
- package/src/messaging/channels/SystemMessageChannel.test.ts +74 -0
- package/src/messaging/channels/SystemMessageChannel.ts +4 -3
- package/src/react/actions/AppActions.test.ts +22 -0
- package/src/react/contexts/AppContext.tsx +1 -1
- package/src/react/reducers/AppReducer.test.ts +42 -0
- package/src/react/reducers/Reducer.test.ts +22 -0
- package/src/react/states/AppState.test.ts +17 -0
- package/src/react/states/State.test.ts +53 -0
- package/src/renderer.test.tsx +88 -0
- package/src/resources/locales/de-DE/translation.json +3 -1
- package/src/resources/locales/en-GB/translation.json +3 -1
- package/src/resources/locales/pl-PL/translation.json +3 -1
- package/src/styles/MaterialThemes.test.ts +29 -0
- package/src/theme/ColorThemes.test.ts +84 -0
- package/src/theme/Theme.test.ts +39 -0
- package/src/views/development/DevelopmentView.test.tsx +157 -0
- package/src/views/home/HomeView.test.tsx +870 -0
- package/src/views/home/HomeView.tsx +6 -0
- package/src/views/settings/SettingsView.test.tsx +330 -0
- package/tests/RootAttributeRemover.tsx +39 -0
- package/tests/TestRenderer.tsx +8 -1
- package/tests/mocks/App.tsx +3 -0
- package/tests/mocks/automations/Helpers.ts +13 -0
- package/tests/mocks/child-process.ts +7 -0
- package/tests/mocks/common/CancellablePromise.ts +4 -0
- package/tests/mocks/common/Delay.ts +4 -0
- package/tests/mocks/common/FileSystem.ts +7 -0
- package/tests/mocks/common/Formatters.ts +10 -0
- package/tests/mocks/common/Helpers.ts +13 -0
- package/tests/mocks/common/Logger.ts +10 -0
- package/tests/mocks/common/Reporter.ts +23 -0
- package/tests/mocks/electron-store.ts +25 -0
- package/tests/mocks/electron.ts +29 -0
- package/tests/mocks/fs-extra.ts +22 -0
- package/tests/mocks/hooks/useCancellablePromises.ts +4 -0
- package/tests/mocks/hooks/useClickCounter.ts +9 -0
- package/tests/mocks/messaging/MessageBus.ts +10 -0
- package/tests/mocks/moment-duration-format.ts +3 -0
- package/tests/mocks/mui-material-styles.ts +12 -0
- package/tests/mocks/puppeteer-core.ts +7 -0
- package/tests/mocks/puppeteer-extra-plugin-stealth.ts +3 -0
- package/tests/mocks/puppeteer-extra.ts +10 -0
- package/tests/mocks/react/contexts/AppContext.tsx +32 -0
- package/tests/mocks/react/contexts/AppThemeContext.tsx +16 -0
- package/tests/mocks/react/contexts/DataContext.tsx +17 -0
- package/tests/mocks/react-dom-client.ts +10 -0
- package/tests/mocks/react-i18next.ts +17 -0
- package/tests/mocks/usehooks-ts.ts +10 -0
- package/tests/mocks/win-version-info.ts +3 -0
- package/tests/mocks/yt-dlp-wrap.ts +14 -0
- package/tsconfig.json +5 -2
- package/src/components/styledTextField/StyledTextField.tsx +0 -28
- package/src/react/hooks/useAppTheme.ts +0 -14
- package/tests/TestMultipleMock.ts +0 -91779
- package/tests/TestPlaylistMock.ts +0 -17384
- /package/tests/{FileMock.ts → mocks/FileMock.ts} +0 -0
package/.eslintrc.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
1
|
+
// {
|
|
2
|
+
// "env": {
|
|
3
|
+
// "browser": true,
|
|
4
|
+
// "es2021": true
|
|
5
|
+
// },
|
|
6
|
+
// "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
|
7
|
+
// "parser": "@typescript-eslint/parser",
|
|
8
|
+
// "parserOptions": {
|
|
9
|
+
// "ecmaVersion": "latest",
|
|
10
|
+
// "sourceType": "module"
|
|
11
|
+
// },
|
|
12
|
+
// "settings": {
|
|
13
|
+
// "react": {
|
|
14
|
+
// "version": "detect"
|
|
15
|
+
// }
|
|
16
|
+
// },
|
|
17
|
+
// "plugins": ["react", "@typescript-eslint"],
|
|
18
|
+
// "rules": {
|
|
19
|
+
// "indent": ["error", 4],
|
|
20
|
+
// "linebreak-style": ["error", "windows"],
|
|
21
|
+
// "quotes": ["error", "double"],
|
|
22
|
+
// "semi": ["error", "always"],
|
|
23
|
+
// "@typescript-eslint/no-var-requires": 0,
|
|
24
|
+
// "@typescript-eslint/no-explicit-any": 0,
|
|
25
|
+
// "@typescript-eslint/no-empty-interface": 0,
|
|
26
|
+
// "react/prop-types": 0,
|
|
27
|
+
// "react/display-name": 0
|
|
28
|
+
// }
|
|
29
|
+
// }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Run Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Node
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
|
|
20
|
+
- name: Enable Corepack (Yarn 4.x support)
|
|
21
|
+
run: corepack enable
|
|
22
|
+
|
|
23
|
+
- name: Prepare Yarn version
|
|
24
|
+
run: corepack prepare yarn@4.4.1 --activate
|
|
25
|
+
|
|
26
|
+
- name: Cache Yarn PnP
|
|
27
|
+
uses: actions/cache@v4
|
|
28
|
+
with:
|
|
29
|
+
path: .yarn/cache
|
|
30
|
+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
31
|
+
restore-keys: |
|
|
32
|
+
${{ runner.os }}-yarn-
|
|
33
|
+
|
|
34
|
+
- name: Install dependencies (Yarn PnP)
|
|
35
|
+
run: yarn install --immutable
|
|
36
|
+
|
|
37
|
+
- name: Run unit tests
|
|
38
|
+
run: yarn test --ci --coverage
|
|
39
|
+
|
|
40
|
+
- name: Upload coverage artifact
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: coverage
|
|
44
|
+
path: coverage
|
|
45
|
+
|
|
46
|
+
- name: Upload coverage to Codecov
|
|
47
|
+
uses: codecov/codecov-action@v5
|
|
48
|
+
with:
|
|
49
|
+
token: 6353cc14-1536-4b0c-bf9c-a0aad51ceff5
|
|
50
|
+
files: ./coverage/lcov.info
|
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
[](https://codecov.io/gh/karenpommeroy/yt-grabber)
|
|
7
|
+
|
|
5
8
|
**YT Grabber** is a robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services.
|
|
6
9
|
|
|
7
10
|
It provides responsive UI to manage your downloads and automation features improve and accelerate download process.
|
package/eslint.config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import react from "eslint-plugin-react";
|
|
2
|
-
import {defineConfig} from "eslint/config";
|
|
2
|
+
import {Config, defineConfig} from "eslint/config";
|
|
3
3
|
import globals from "globals";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import {fileURLToPath} from "node:url";
|
|
@@ -9,6 +9,10 @@ import js from "@eslint/js";
|
|
|
9
9
|
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
10
10
|
import tsParser from "@typescript-eslint/parser";
|
|
11
11
|
|
|
12
|
+
type EslintConfig = Config & {
|
|
13
|
+
extends?: any[];
|
|
14
|
+
};
|
|
15
|
+
|
|
12
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
17
|
const __dirname = path.dirname(__filename);
|
|
14
18
|
const compat = new FlatCompat({
|
|
@@ -17,7 +21,49 @@ const compat = new FlatCompat({
|
|
|
17
21
|
allConfig: js.configs.all
|
|
18
22
|
});
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const TestConfig: EslintConfig = {
|
|
25
|
+
extends: compat.extends(
|
|
26
|
+
// "eslint:recommended",
|
|
27
|
+
"plugin:react/recommended",
|
|
28
|
+
// "plugin:@typescript-eslint/recommended",
|
|
29
|
+
"prettier",
|
|
30
|
+
),
|
|
31
|
+
plugins: {
|
|
32
|
+
react,
|
|
33
|
+
"@typescript-eslint": typescriptEslint as any,
|
|
34
|
+
},
|
|
35
|
+
languageOptions: {
|
|
36
|
+
globals: {
|
|
37
|
+
...globals.browser,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
parser: tsParser,
|
|
41
|
+
ecmaVersion: "latest",
|
|
42
|
+
sourceType: "module",
|
|
43
|
+
},
|
|
44
|
+
settings: {
|
|
45
|
+
react: {
|
|
46
|
+
version: "detect",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
files: ["**/*.test.{ts,tsx,js,jsx}"],
|
|
50
|
+
rules: {
|
|
51
|
+
indent: ["warn", 4],
|
|
52
|
+
"linebreak-style": ["warn", "windows"],
|
|
53
|
+
quotes: ["warn", "double"],
|
|
54
|
+
semi: ["warn", "always"],
|
|
55
|
+
"@typescript-eslint/no-unused-vars": ["warn"],
|
|
56
|
+
"@typescript-eslint/no-var-requires": 0,
|
|
57
|
+
"@typescript-eslint/no-explicit-any": 0,
|
|
58
|
+
"@typescript-eslint/no-empty-interface": 0,
|
|
59
|
+
"react/prop-types": 0,
|
|
60
|
+
"react/display-name": 0,
|
|
61
|
+
"react/react-in-jsx-scope": 0,
|
|
62
|
+
"import/no-commonjs": "off",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const BaseConfig: EslintConfig = {
|
|
21
67
|
extends: compat.extends(
|
|
22
68
|
"eslint:recommended",
|
|
23
69
|
"plugin:react/recommended",
|
|
@@ -46,6 +92,10 @@ export default defineConfig([{
|
|
|
46
92
|
},
|
|
47
93
|
},
|
|
48
94
|
|
|
95
|
+
files: [
|
|
96
|
+
"**/!(*.test).{ts,tsx,js,jsx}",
|
|
97
|
+
],
|
|
98
|
+
|
|
49
99
|
rules: {
|
|
50
100
|
indent: ["warn", 4],
|
|
51
101
|
"linebreak-style": ["warn", "windows"],
|
|
@@ -59,4 +109,9 @@ export default defineConfig([{
|
|
|
59
109
|
"react/display-name": 0,
|
|
60
110
|
"react/react-in-jsx-scope": 0,
|
|
61
111
|
},
|
|
62
|
-
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default defineConfig([
|
|
115
|
+
BaseConfig,
|
|
116
|
+
TestConfig
|
|
117
|
+
]);
|
package/jest.config.ts
CHANGED
|
@@ -11,14 +11,30 @@ const config: Config = {
|
|
|
11
11
|
],
|
|
12
12
|
moduleNameMapper: {
|
|
13
13
|
"\\.(css|less|scss|sass|styl)$": "identity-obj-proxy",
|
|
14
|
-
"\\.(jpg|jpeg|png|gif|svg)$": "<rootDir>/tests/FileMock.ts",
|
|
15
|
-
"^@tests/(.*)$": "<rootDir>/tests/$1"
|
|
14
|
+
"\\.(jpg|jpeg|png|gif|svg)$": "<rootDir>/tests/mocks/FileMock.ts",
|
|
15
|
+
"^@tests/(.*)$": "<rootDir>/tests/$1",
|
|
16
|
+
"^@app/(.*)$": "<rootDir>/src/$1",
|
|
16
17
|
},
|
|
17
18
|
setupFilesAfterEnv: [
|
|
18
19
|
"@testing-library/jest-dom",
|
|
19
20
|
"<rootDir>/jest.setup.ts",
|
|
20
21
|
],
|
|
21
22
|
testPathIgnorePatterns: ["/node_modules/", "/dist/", "/out/", "/e2e/"],
|
|
23
|
+
reporters: [
|
|
24
|
+
"default",
|
|
25
|
+
],
|
|
26
|
+
collectCoverage: true,
|
|
27
|
+
collectCoverageFrom: [
|
|
28
|
+
"src/**/*.{ts,tsx}",
|
|
29
|
+
"!src/**/*.d.ts",
|
|
30
|
+
"!src/**/index.ts",
|
|
31
|
+
],
|
|
32
|
+
coverageDirectory: "<rootDir>/coverage",
|
|
33
|
+
coverageProvider: "v8",
|
|
34
|
+
coverageReporters: [
|
|
35
|
+
"json",
|
|
36
|
+
"lcov",
|
|
37
|
+
],
|
|
22
38
|
};
|
|
23
39
|
|
|
24
40
|
export default config;
|
package/jest.setup.ts
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
import {TextDecoder, TextEncoder} from "util";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import electronMock from "@tests/mocks/electron";
|
|
4
|
+
import storeMock from "@tests/mocks/electron-store";
|
|
5
|
+
import reactI18nMock from "@tests/mocks/react-i18next";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
set: jest.fn(),
|
|
9
|
-
delete: jest.fn(),
|
|
10
|
-
clear: jest.fn(),
|
|
11
|
-
has: jest.fn().mockReturnValue(true),
|
|
12
|
-
onDidAnyChange: jest.fn().mockReturnValue(jest.fn()),
|
|
13
|
-
onDidChange: jest.fn().mockReturnValue(jest.fn())
|
|
14
|
-
};
|
|
7
|
+
global.TextEncoder = TextEncoder as typeof global.TextEncoder;
|
|
8
|
+
global.TextDecoder = TextDecoder as typeof global.TextDecoder;
|
|
15
9
|
|
|
16
10
|
Object.defineProperty(process, "resourcesPath", {
|
|
17
|
-
value: "/
|
|
11
|
+
value: "./src/resources",
|
|
18
12
|
writable: false,
|
|
19
13
|
});
|
|
20
14
|
|
|
21
|
-
jest.mock("
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
on: jest.fn(),
|
|
25
|
-
off: jest.fn(),
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
jest.mock("electron-store", () => {
|
|
30
|
-
return jest.fn(() => mockStore);
|
|
31
|
-
});
|
|
15
|
+
jest.mock("react-i18next", () => reactI18nMock);
|
|
16
|
+
jest.mock("electron", () => electronMock);
|
|
17
|
+
jest.mock("electron-store", () => storeMock);
|
|
32
18
|
|
|
33
|
-
(global as any).store =
|
|
19
|
+
(global as any).store = storeMock();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
4
4
|
"description": "Youtube Grabber - robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"youtube",
|
|
@@ -171,6 +171,7 @@
|
|
|
171
171
|
"identity-obj-proxy": "^3.0.0",
|
|
172
172
|
"jest": "^30.2.0",
|
|
173
173
|
"jest-environment-jsdom": "^30.2.0",
|
|
174
|
+
"jest-html-reporter": "^4.3.0",
|
|
174
175
|
"jiti": "^2.6.1",
|
|
175
176
|
"mini-css-extract-plugin": "^2.9.4",
|
|
176
177
|
"prettier": "^3.6.2",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
|
|
3
|
+
import {getProfilePath} from "../common/FileSystem";
|
|
4
|
+
import {waitFor} from "../common/Helpers";
|
|
5
|
+
import {createInput, createPage} from "../common/TestHelpers";
|
|
6
|
+
import {clearInput, navigateToPage, setCookies} from "./Helpers";
|
|
7
|
+
|
|
8
|
+
jest.mock("fs-extra", () => require("@tests/mocks/fs-extra"));
|
|
9
|
+
jest.mock("../common/FileSystem", () => require("@tests/mocks/common/FileSystem"));
|
|
10
|
+
jest.mock("../common/Helpers", () => require("@tests/mocks/common/Helpers"));
|
|
11
|
+
|
|
12
|
+
describe("automation helpers", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
(getProfilePath as jest.Mock).mockReturnValue("/profile");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("navigateToPage forwards url and timeout options", async () => {
|
|
19
|
+
const page = createPage();
|
|
20
|
+
|
|
21
|
+
await navigateToPage("https://example.com", page);
|
|
22
|
+
|
|
23
|
+
expect(page.goto).toHaveBeenCalledWith("https://example.com", {
|
|
24
|
+
waitUntil: ["networkidle0", "domcontentloaded", "load"],
|
|
25
|
+
timeout: 15000,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("clearInput selects content and presses backspace", async () => {
|
|
30
|
+
const page = createPage();
|
|
31
|
+
const input = createInput();
|
|
32
|
+
|
|
33
|
+
await clearInput(input, page);
|
|
34
|
+
|
|
35
|
+
expect((input as any).click).toHaveBeenCalledWith({clickCount: 3});
|
|
36
|
+
expect(page.keyboard.press).toHaveBeenCalledWith("Backspace");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("setCookies caches fresh cookies when cache empty", async () => {
|
|
40
|
+
(fs.readJSONSync as jest.Mock).mockReturnValue(undefined);
|
|
41
|
+
const pageCookies = [{name: "session", value: "abc"}];
|
|
42
|
+
const page = createPage();
|
|
43
|
+
page.cookies.mockResolvedValue(pageCookies);
|
|
44
|
+
|
|
45
|
+
await setCookies(page);
|
|
46
|
+
|
|
47
|
+
expect(waitFor).toHaveBeenCalledWith(3000);
|
|
48
|
+
expect(page.cookies).toHaveBeenCalled();
|
|
49
|
+
expect(fs.writeJSONSync).toHaveBeenCalledWith("/profile/cookies.json", pageCookies, {spaces: 2});
|
|
50
|
+
expect(page.setCookie).toHaveBeenCalledWith(...pageCookies);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("setCookies reuses cached cookies when available", async () => {
|
|
54
|
+
const cachedCookies = [{name: "session", value: "cached"}];
|
|
55
|
+
(fs.readJSONSync as jest.Mock).mockReturnValue(cachedCookies);
|
|
56
|
+
const page = createPage();
|
|
57
|
+
|
|
58
|
+
await setCookies(page);
|
|
59
|
+
|
|
60
|
+
expect(waitFor).not.toHaveBeenCalled();
|
|
61
|
+
expect(page.cookies).not.toHaveBeenCalled();
|
|
62
|
+
expect(page.setCookie).toHaveBeenCalledWith(...cachedCookies);
|
|
63
|
+
expect(fs.writeJSONSync).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {TimeoutError} from "puppeteer-core";
|
|
2
|
+
import puppeteer from "puppeteer-extra";
|
|
3
|
+
|
|
4
|
+
import {UserAgent} from "../common/PuppeteerOptions";
|
|
5
|
+
import {Reporter} from "../common/Reporter";
|
|
6
|
+
import {
|
|
7
|
+
createBrowserMock, createElements, createI18n, createPageMock, createYoutubeParams
|
|
8
|
+
} from "../common/TestHelpers";
|
|
9
|
+
import {navigateToPage, setCookies} from "./Helpers";
|
|
10
|
+
import {
|
|
11
|
+
AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector
|
|
12
|
+
} from "./Selectors";
|
|
13
|
+
import execute from "./Youtube";
|
|
14
|
+
|
|
15
|
+
jest.mock("./Helpers", () => require("@tests/mocks/automations/Helpers"));
|
|
16
|
+
jest.mock("puppeteer-core", () => require("@tests/mocks/puppeteer-core"));
|
|
17
|
+
jest.mock("puppeteer-extra", () => require("@tests/mocks/puppeteer-extra"));
|
|
18
|
+
jest.mock("puppeteer-extra-plugin-stealth", () => require("@tests/mocks/puppeteer-extra-plugin-stealth"));
|
|
19
|
+
jest.mock("../common/Reporter", () => require("@tests/mocks/common/Reporter"));
|
|
20
|
+
|
|
21
|
+
const navigateToPageMock = navigateToPage as jest.Mock;
|
|
22
|
+
const setCookiesMock = setCookies as jest.Mock;
|
|
23
|
+
const launchMock = puppeteer.launch as jest.Mock;
|
|
24
|
+
const ReporterMock = new Reporter(() => {});
|
|
25
|
+
const reporterFinishMock = ReporterMock.finish as jest.Mock;
|
|
26
|
+
|
|
27
|
+
describe("Youtube automation", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("collects album links via filtered listing", async () => {
|
|
33
|
+
const page = createPageMock();
|
|
34
|
+
page.waitForSelector.mockImplementation((selector: string) => {
|
|
35
|
+
if (selector.includes(AlbumsHrefSelector)) {
|
|
36
|
+
return Promise.resolve({evaluate: jest.fn().mockResolvedValue("channel/albums")});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (selector.includes(AlbumFilterSelector)) {
|
|
40
|
+
return Promise.resolve({click: jest.fn()});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Promise.reject(new Error(`Unexpected selector: ${selector}`));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
page.$$eval.mockImplementation((selector: string, callback: (elements: Array<{getAttribute: () => string;}>) => string[]) => {
|
|
47
|
+
if (selector === "xpath/" + AlbumLinkSelector) {
|
|
48
|
+
return Promise.resolve(callback(createElements(["album/one", "album/two"])));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (selector === "xpath/" + AlbumsDirectLinkSelector) {
|
|
52
|
+
return Promise.resolve(callback(createElements([])));
|
|
53
|
+
}
|
|
54
|
+
return Promise.reject(new Error(`Unexpected selector: ${selector}`));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const browser = createBrowserMock(page);
|
|
58
|
+
launchMock.mockResolvedValue(browser);
|
|
59
|
+
navigateToPageMock.mockResolvedValue(undefined);
|
|
60
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
61
|
+
|
|
62
|
+
await execute({
|
|
63
|
+
params: createYoutubeParams(),
|
|
64
|
+
options: {},
|
|
65
|
+
i18n: createI18n(),
|
|
66
|
+
onUpdate: jest.fn(),
|
|
67
|
+
signal: new AbortController().signal
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(page.setUserAgent).toHaveBeenCalledWith(UserAgent);
|
|
71
|
+
expect(navigateToPageMock).toHaveBeenCalledWith("https://music.youtube.com", page);
|
|
72
|
+
expect(reporterFinishMock).toHaveBeenCalledWith("done", expect.objectContaining({
|
|
73
|
+
errors: [],
|
|
74
|
+
warnings: [],
|
|
75
|
+
values: [
|
|
76
|
+
"https://music.youtube.com/album/one",
|
|
77
|
+
"https://music.youtube.com/album/two",
|
|
78
|
+
],
|
|
79
|
+
sources: ["https://music.youtube.com/channel/UC123"],
|
|
80
|
+
}));
|
|
81
|
+
expect(page.close).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(browser.close).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("falls back to direct album links when filters fail", async () => {
|
|
86
|
+
const page = createPageMock();
|
|
87
|
+
page.waitForSelector.mockRejectedValue(new Error("Not available"));
|
|
88
|
+
page.$$eval.mockImplementation((selector: string, callback: (elements: Array<{getAttribute: () => string;}>) => string[]) => {
|
|
89
|
+
if (selector === "xpath/" + AlbumsDirectLinkSelector) {
|
|
90
|
+
return Promise.resolve(callback(createElements(["direct/one"])));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Promise.reject(new Error(`Unexpected selector ${selector}`));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const browser = createBrowserMock(page);
|
|
97
|
+
launchMock.mockResolvedValue(browser);
|
|
98
|
+
navigateToPageMock.mockResolvedValue(undefined);
|
|
99
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
100
|
+
|
|
101
|
+
await execute({
|
|
102
|
+
params: createYoutubeParams(),
|
|
103
|
+
options: {},
|
|
104
|
+
i18n: createI18n(),
|
|
105
|
+
onUpdate: jest.fn(),
|
|
106
|
+
signal: new AbortController().signal,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(page.$$eval).toHaveBeenCalledWith("xpath/" + AlbumsDirectLinkSelector, expect.any(Function));
|
|
110
|
+
expect(reporterFinishMock).toHaveBeenCalledWith("done", expect.objectContaining({
|
|
111
|
+
values: ["https://music.youtube.com/direct/one"],
|
|
112
|
+
errors: [],
|
|
113
|
+
warnings: [],
|
|
114
|
+
}));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("reports timeout errors", async () => {
|
|
118
|
+
const page = createPageMock();
|
|
119
|
+
const browser = createBrowserMock(page);
|
|
120
|
+
launchMock.mockResolvedValue(browser);
|
|
121
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
122
|
+
navigateToPageMock.mockRejectedValue(new TimeoutError("timeout"));
|
|
123
|
+
|
|
124
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
125
|
+
|
|
126
|
+
await execute({
|
|
127
|
+
params: createYoutubeParams(),
|
|
128
|
+
options: {},
|
|
129
|
+
i18n: createI18n(),
|
|
130
|
+
onUpdate: jest.fn(),
|
|
131
|
+
signal: new AbortController().signal,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const [, result] = reporterFinishMock.mock.calls[0];
|
|
135
|
+
|
|
136
|
+
expect(result.errors).toEqual([
|
|
137
|
+
{
|
|
138
|
+
title: "exceptionTimeout",
|
|
139
|
+
description: "exceptionTimeoutText",
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
expect(result.warnings ?? []).toEqual([]);
|
|
143
|
+
expect(result.values ?? []).toEqual([]);
|
|
144
|
+
expect(page.close).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(browser.close).toHaveBeenCalledTimes(1);
|
|
146
|
+
consoleErrorSpy.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("reports navigation detach as warning", async () => {
|
|
150
|
+
const page = createPageMock();
|
|
151
|
+
const browser = createBrowserMock(page);
|
|
152
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
153
|
+
|
|
154
|
+
launchMock.mockResolvedValue(browser);
|
|
155
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
156
|
+
navigateToPageMock.mockRejectedValue(new Error("Navigating frame was detached"));
|
|
157
|
+
|
|
158
|
+
await execute({
|
|
159
|
+
params: createYoutubeParams(),
|
|
160
|
+
options: {},
|
|
161
|
+
i18n: createI18n(),
|
|
162
|
+
onUpdate: jest.fn(),
|
|
163
|
+
signal: new AbortController().signal,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const [, result] = reporterFinishMock.mock.calls[0];
|
|
167
|
+
|
|
168
|
+
expect(result.errors ?? []).toEqual([]);
|
|
169
|
+
expect(result.warnings).toEqual([
|
|
170
|
+
{
|
|
171
|
+
title: "exceptionGetYoutubeUrls",
|
|
172
|
+
description: "exceptionGetYoutubeUrlsText",
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
expect(page.close).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(browser.close).toHaveBeenCalledTimes(1);
|
|
177
|
+
consoleErrorSpy.mockRestore();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("reports generic errors", async () => {
|
|
181
|
+
const page = createPageMock();
|
|
182
|
+
const browser = createBrowserMock(page);
|
|
183
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
184
|
+
|
|
185
|
+
launchMock.mockResolvedValue(browser);
|
|
186
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
187
|
+
navigateToPageMock.mockRejectedValue(new Error("boom"));
|
|
188
|
+
|
|
189
|
+
await execute({
|
|
190
|
+
params: createYoutubeParams(),
|
|
191
|
+
options: {},
|
|
192
|
+
i18n: createI18n(),
|
|
193
|
+
onUpdate: jest.fn(),
|
|
194
|
+
signal: new AbortController().signal,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const [, result] = reporterFinishMock.mock.calls[0];
|
|
198
|
+
|
|
199
|
+
expect(result.errors).toEqual([
|
|
200
|
+
{
|
|
201
|
+
title: "exceptionGetYoutubeUrls",
|
|
202
|
+
description: "exceptionGetYoutubeUrlsText",
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
expect(result.warnings ?? []).toEqual([]);
|
|
206
|
+
expect(page.close).toHaveBeenCalledTimes(1);
|
|
207
|
+
expect(browser.close).toHaveBeenCalledTimes(1);
|
|
208
|
+
consoleErrorSpy.mockRestore();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("cancels operation properly", async () => {
|
|
212
|
+
const page = createPageMock();
|
|
213
|
+
const abortController = new AbortController();
|
|
214
|
+
|
|
215
|
+
page.waitForSelector.mockImplementation(() => {
|
|
216
|
+
abortController.abort();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const browser = createBrowserMock(page);
|
|
220
|
+
launchMock.mockResolvedValue(browser);
|
|
221
|
+
navigateToPageMock.mockResolvedValue(undefined);
|
|
222
|
+
setCookiesMock.mockResolvedValue(undefined);
|
|
223
|
+
|
|
224
|
+
expect(execute({
|
|
225
|
+
params: createYoutubeParams(),
|
|
226
|
+
options: {},
|
|
227
|
+
i18n: createI18n(),
|
|
228
|
+
onUpdate: jest.fn(),
|
|
229
|
+
signal: abortController.signal,
|
|
230
|
+
})).rejects.toThrow("aborted");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {i18n as i18next} from "i18next";
|
|
2
|
-
import {
|
|
2
|
+
import {merge} from "lodash-es";
|
|
3
3
|
import {Browser, LaunchOptions, Page, TimeoutError} from "puppeteer-core";
|
|
4
4
|
import puppeteer from "puppeteer-extra";
|
|
5
5
|
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
@@ -32,7 +32,7 @@ export const execute = async (parameters: MessageHandlerParams) => {
|
|
|
32
32
|
abortPromise,
|
|
33
33
|
]);
|
|
34
34
|
} catch (error: any) {
|
|
35
|
-
const result: GetYoutubeResult = {errors: [], sources: params.values};
|
|
35
|
+
const result: GetYoutubeResult = {warnings: [], errors: [], sources: params.values};
|
|
36
36
|
if (error.message === "aborted") {
|
|
37
37
|
throw error;
|
|
38
38
|
}
|
|
@@ -55,7 +55,6 @@ export const execute = async (parameters: MessageHandlerParams) => {
|
|
|
55
55
|
|
|
56
56
|
const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18next, onUpdate: (data: ProgressInfo<GetYoutubeResult>) => void) => {
|
|
57
57
|
const result: GetYoutubeResult = {warnings: [], errors: [], values: [], sources: params.values};
|
|
58
|
-
|
|
59
58
|
await i18n.changeLanguage(params.lang);
|
|
60
59
|
|
|
61
60
|
reporter = new Reporter(onUpdate);
|
|
@@ -81,8 +80,7 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
|
|
|
81
80
|
|
|
82
81
|
albumFilterButton.click();
|
|
83
82
|
await page.waitForNetworkIdle();
|
|
84
|
-
|
|
85
|
-
const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements: Element[]) => map(elements, (el) => el.getAttribute("href")));
|
|
83
|
+
const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements: Element[]) => elements.map((el) => el.getAttribute("href")));
|
|
86
84
|
|
|
87
85
|
for (const item of items) {
|
|
88
86
|
results.push(`${params.url}/${item}`);
|
|
@@ -90,7 +88,7 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
|
|
|
90
88
|
|
|
91
89
|
return results;
|
|
92
90
|
} catch (error) {
|
|
93
|
-
const items = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements: Element[]) => map(
|
|
91
|
+
const items = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements: Element[]) => elements.map((el) => el.getAttribute("href")));
|
|
94
92
|
|
|
95
93
|
for (const item of items) {
|
|
96
94
|
results.push(`${params.url}/${item}`);
|