Improved testing flow
This commit is contained in:
15
README.md
15
README.md
@@ -232,6 +232,21 @@ Run all tests (WASM integration + E2E):
|
|||||||
|
|
||||||
This builds the web app, compiles the WASM binary, runs the WASM integration tests, and then runs the full E2E suite. On first run it will prompt to download test assets (~190 MB total) to `tests/cached_assets/`. Tests that require missing assets are skipped.
|
This builds the web app, compiles the WASM binary, runs the WASM integration tests, and then runs the full E2E suite. On first run it will prompt to download test assets (~190 MB total) to `tests/cached_assets/`. Tests that require missing assets are skipped.
|
||||||
|
|
||||||
|
Available flags:
|
||||||
|
|
||||||
|
- `--headed` — run with a visible browser window (also sets `SLOW_MO=1000` for 1s delay between actions)
|
||||||
|
- `--test <pattern>` — filter E2E tests by name (maps to Playwright `--grep`)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test.sh --headed
|
||||||
|
./test.sh --test "NickelMenu"
|
||||||
|
./test.sh --headed --test "back navigation"
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional Playwright arguments can be appended after the flags.
|
||||||
|
|
||||||
### E2E tests (Playwright)
|
### E2E tests (Playwright)
|
||||||
|
|
||||||
The E2E tests cover all major user flows:
|
The E2E tests cover all major user flows:
|
||||||
|
|||||||
29
test.sh
29
test.sh
@@ -1,6 +1,33 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
HEADED=""
|
||||||
|
GREP=""
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--headed)
|
||||||
|
HEADED="--headed"
|
||||||
|
export SLOW_MO=1000
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--test)
|
||||||
|
GREP="--grep"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ "$GREP" == "--grep" ]]; then
|
||||||
|
GREP="--grep $1"
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
EXTRA_ARGS+=("$1")
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
CACHED_ASSETS="$SCRIPT_DIR/tests/cached_assets"
|
CACHED_ASSETS="$SCRIPT_DIR/tests/cached_assets"
|
||||||
|
|
||||||
@@ -67,4 +94,4 @@ if [ ! -d "node_modules" ]; then
|
|||||||
npx playwright install --with-deps
|
npx playwright install --with-deps
|
||||||
fi
|
fi
|
||||||
|
|
||||||
npm test
|
npx playwright test $HEADED $GREP "${EXTRA_ARGS[@]}"
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ test.describe('NickelMenu', () => {
|
|||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
|
||||||
// Mode selection: NickelMenu should be pre-selected (checked in HTML)
|
// Select NickelMenu and continue
|
||||||
await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked();
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step
|
// NickelMenu configure step
|
||||||
@@ -104,8 +104,8 @@ test.describe('NickelMenu', () => {
|
|||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
|
||||||
// Mode selection
|
// Select NickelMenu and continue
|
||||||
await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked();
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step — select "Install NickelMenu with preset"
|
// NickelMenu configure step — select "Install NickelMenu with preset"
|
||||||
@@ -158,6 +158,7 @@ test.describe('NickelMenu', () => {
|
|||||||
await connectMockDevice(page, { hasNickelMenu: false });
|
await connectMockDevice(page, { hasNickelMenu: false });
|
||||||
|
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// Select "Install NickelMenu with preset"
|
// Select "Install NickelMenu with preset"
|
||||||
@@ -193,6 +194,7 @@ test.describe('NickelMenu', () => {
|
|||||||
test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot');
|
test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot');
|
||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
||||||
|
|
||||||
@@ -228,6 +230,7 @@ test.describe('NickelMenu', () => {
|
|||||||
test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot');
|
test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot');
|
||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
||||||
|
|
||||||
@@ -246,8 +249,8 @@ test.describe('NickelMenu', () => {
|
|||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||||
|
|
||||||
// NickelMenu is pre-selected
|
// Select NickelMenu and continue
|
||||||
await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked();
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step
|
// NickelMenu configure step
|
||||||
@@ -317,8 +320,9 @@ test.describe('NickelMenu', () => {
|
|||||||
|
|
||||||
await connectMockDevice(page, { hasNickelMenu: false });
|
await connectMockDevice(page, { hasNickelMenu: false });
|
||||||
|
|
||||||
// Continue to mode selection
|
// Continue to mode selection → select NickelMenu
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step
|
// NickelMenu configure step
|
||||||
@@ -349,8 +353,9 @@ test.describe('NickelMenu', () => {
|
|||||||
|
|
||||||
await connectMockDevice(page, { hasNickelMenu: true });
|
await connectMockDevice(page, { hasNickelMenu: true });
|
||||||
|
|
||||||
// Continue to mode selection
|
// Continue to mode selection → select NickelMenu
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step
|
// NickelMenu configure step
|
||||||
@@ -404,6 +409,7 @@ test.describe('NickelMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// Select remove
|
// Select remove
|
||||||
@@ -455,6 +461,7 @@ test.describe('NickelMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// NickelMenu configure step
|
// NickelMenu configure step
|
||||||
@@ -493,6 +500,7 @@ test.describe('NickelMenu', () => {
|
|||||||
test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)');
|
test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)');
|
||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// Select preset → features
|
// Select preset → features
|
||||||
@@ -542,6 +550,7 @@ test.describe('NickelMenu', () => {
|
|||||||
test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)');
|
test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)');
|
||||||
|
|
||||||
await goToManualMode(page);
|
await goToManualMode(page);
|
||||||
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
// Preset path: enable some features
|
// Preset path: enable some features
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module.exports = defineConfig({
|
|||||||
actionTimeout: 10_000,
|
actionTimeout: 10_000,
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
args: ['--disable-dev-shm-usage'],
|
args: ['--disable-dev-shm-usage'],
|
||||||
serialMo: parseInt(process.env.SLOW_MO || '0', 10),
|
slowMo: parseInt(process.env.SLOW_MO || '0', 10),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ test('manual nickelmenu', async ({ page }, testInfo) => {
|
|||||||
// Click "Build downloadable archive" to enter manual mode
|
// Click "Build downloadable archive" to enter manual mode
|
||||||
await page.click('#btn-manual');
|
await page.click('#btn-manual');
|
||||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||||
await shot(page, dir, '01-mode-selection', testInfo);
|
|
||||||
|
|
||||||
// Select NickelMenu → config
|
// Select NickelMenu, screenshot, then proceed
|
||||||
await page.click('input[name="mode"][value="nickelmenu"]');
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
|
await shot(page, dir, '01-mode-selection', testInfo);
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
||||||
await shot(page, dir, '02-nickelmenu-config', testInfo);
|
await shot(page, dir, '02-nickelmenu-config', testInfo);
|
||||||
@@ -90,10 +90,10 @@ test('manual patches', async ({ page }, testInfo) => {
|
|||||||
// Click "Build downloadable archive" to enter manual mode
|
// Click "Build downloadable archive" to enter manual mode
|
||||||
await page.click('#btn-manual');
|
await page.click('#btn-manual');
|
||||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||||
await shot(page, dir, '01-mode-selection', testInfo);
|
|
||||||
|
|
||||||
// Select Patches → version selection
|
// Select Patches, then screenshot mode selection before proceeding
|
||||||
await page.click('input[name="mode"][value="patches"]');
|
await page.click('input[name="mode"][value="patches"]');
|
||||||
|
await shot(page, dir, '01-mode-selection', testInfo);
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-manual-version')).not.toBeHidden();
|
await expect(page.locator('#step-manual-version')).not.toBeHidden();
|
||||||
await shot(page, dir, '02-version-selection', testInfo);
|
await shot(page, dir, '02-version-selection', testInfo);
|
||||||
@@ -160,13 +160,11 @@ test('connected nickelmenu', async ({ page }, testInfo) => {
|
|||||||
await expect(page.locator('#step-device')).not.toBeHidden();
|
await expect(page.locator('#step-device')).not.toBeHidden();
|
||||||
await shot(page, dir, '03-device', testInfo);
|
await shot(page, dir, '03-device', testInfo);
|
||||||
|
|
||||||
// Mode selection
|
// Mode selection — select NickelMenu, screenshot, then proceed
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||||
await shot(page, dir, '04-mode-selection', testInfo);
|
|
||||||
|
|
||||||
// NickelMenu config
|
|
||||||
await page.click('input[name="mode"][value="nickelmenu"]');
|
await page.click('input[name="mode"][value="nickelmenu"]');
|
||||||
|
await shot(page, dir, '04-mode-selection', testInfo);
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
|
||||||
await shot(page, dir, '05-nickelmenu-config', testInfo);
|
await shot(page, dir, '05-nickelmenu-config', testInfo);
|
||||||
@@ -222,13 +220,11 @@ test('connected patches', async ({ page }, testInfo) => {
|
|||||||
await expect(page.locator('#step-device')).not.toBeHidden();
|
await expect(page.locator('#step-device')).not.toBeHidden();
|
||||||
await shot(page, dir, '03-device', testInfo);
|
await shot(page, dir, '03-device', testInfo);
|
||||||
|
|
||||||
// Mode selection
|
// Mode selection — select Patches, screenshot, then proceed
|
||||||
await page.click('#btn-device-next');
|
await page.click('#btn-device-next');
|
||||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||||
await shot(page, dir, '04-mode-selection', testInfo);
|
|
||||||
|
|
||||||
// Patches config
|
|
||||||
await page.click('input[name="mode"][value="patches"]');
|
await page.click('input[name="mode"][value="patches"]');
|
||||||
|
await shot(page, dir, '04-mode-selection', testInfo);
|
||||||
await page.click('#btn-mode-next');
|
await page.click('#btn-mode-next');
|
||||||
await expect(page.locator('#step-patches')).not.toBeHidden();
|
await expect(page.locator('#step-patches')).not.toBeHidden();
|
||||||
await shot(page, dir, '05-patches-config', testInfo);
|
await shot(page, dir, '05-patches-config', testInfo);
|
||||||
|
|||||||
@@ -193,8 +193,8 @@
|
|||||||
<section id="step-mode" class="step" hidden>
|
<section id="step-mode" class="step" hidden>
|
||||||
<p>What would you like to do?</p>
|
<p>What would you like to do?</p>
|
||||||
<div class="selection-cards" role="radiogroup" aria-label="Mode selection">
|
<div class="selection-cards" role="radiogroup" aria-label="Mode selection">
|
||||||
<label class="selection-card selection-card--selected selection-card--recommended">
|
<label class="selection-card selection-card--recommended">
|
||||||
<input type="radio" name="mode" value="nickelmenu" checked>
|
<input type="radio" name="mode" value="nickelmenu">
|
||||||
<div class="selection-card-body">
|
<div class="selection-card-body">
|
||||||
<div class="selection-card-title">Install or remove NickelMenu</div>
|
<div class="selection-card-title">Install or remove NickelMenu</div>
|
||||||
<div class="recommended-label">Recommended</div>
|
<div class="recommended-label">Recommended</div>
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
<p id="mode-patches-hint" class="fallback-hint" hidden>Custom patches are not available for your software version. You can still install NickelMenu and choose what you want to do with your Kobo.</p>
|
<p id="mode-patches-hint" class="fallback-hint" hidden>Custom patches are not available for your software version. You can still install NickelMenu and choose what you want to do with your Kobo.</p>
|
||||||
<div class="step-actions">
|
<div class="step-actions">
|
||||||
<button id="btn-mode-back" class="secondary">‹ Back</button>
|
<button id="btn-mode-back" class="secondary">‹ Back</button>
|
||||||
<button id="btn-mode-next" class="primary">Continue ›</button>
|
<button id="btn-mode-next" class="primary" disabled>Continue ›</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { KoboPatchRunner } from './services/patch-runner.js';
|
|||||||
import { NickelMenuInstaller, ALL_FEATURES } from '../nickelmenu/installer.js';
|
import { NickelMenuInstaller, ALL_FEATURES } from '../nickelmenu/installer.js';
|
||||||
import { TL } from './strings.js';
|
import { TL } from './strings.js';
|
||||||
import { isEnabled as analyticsEnabled, track } from './analytics.js';
|
import { isEnabled as analyticsEnabled, track } from './analytics.js';
|
||||||
import { $, $q, populateSelect } from './dom.js';
|
import { $, $q, $qa, populateSelect } from './dom.js';
|
||||||
import { showStep, setNavLabels, setNavStep, hideNav, showNav, stepHistory, setupCardRadios } from './nav.js';
|
import { showStep, setNavLabels, setNavStep, hideNav, showNav, stepHistory, setupCardRadios } from './nav.js';
|
||||||
import { initNickelMenu } from './flows/nickelmenu-flow.js';
|
import { initNickelMenu } from './flows/nickelmenu-flow.js';
|
||||||
import { initPatchesFlow } from './flows/patches-flow.js';
|
import { initPatchesFlow } from './flows/patches-flow.js';
|
||||||
@@ -131,7 +131,7 @@ const nm = initNickelMenu(state);
|
|||||||
const patches = initPatchesFlow(state);
|
const patches = initPatchesFlow(state);
|
||||||
|
|
||||||
// Wire up card-radio interactivity for mode selection and NM option cards.
|
// Wire up card-radio interactivity for mode selection and NM option cards.
|
||||||
setupCardRadios(stepMode, 'selection-card--selected');
|
setupCardRadios(stepMode, 'selection-card--selected', () => { btnModeNext.disabled = false; });
|
||||||
setupCardRadios($('step-nickelmenu'), 'selection-card--selected');
|
setupCardRadios($('step-nickelmenu'), 'selection-card--selected');
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -181,6 +181,14 @@ state.showError = showError;
|
|||||||
|
|
||||||
function goToModeSelection() {
|
function goToModeSelection() {
|
||||||
nm.resetNickelMenuState();
|
nm.resetNickelMenuState();
|
||||||
|
btnModeNext.disabled = true;
|
||||||
|
|
||||||
|
// Clear any previous mode selection so the user must pick again.
|
||||||
|
for (const radio of $qa('input[name="mode"]', stepMode)) {
|
||||||
|
radio.checked = false;
|
||||||
|
radio.closest('.selection-card')?.classList.remove('selection-card--selected');
|
||||||
|
}
|
||||||
|
|
||||||
const patchesRadio = $q('input[value="patches"]', stepMode);
|
const patchesRadio = $q('input[value="patches"]', stepMode);
|
||||||
const patchesCard = patchesRadio.closest('.selection-card');
|
const patchesCard = patchesRadio.closest('.selection-card');
|
||||||
const autoModeNoPatchesAvailable = !state.manualMode && (!state.patchesLoaded || !state.firmwareURL);
|
const autoModeNoPatchesAvailable = !state.manualMode && (!state.patchesLoaded || !state.firmwareURL);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function showNav() {
|
|||||||
* When a radio inside a <label> is checked, the label gets `selectedClass`;
|
* When a radio inside a <label> is checked, the label gets `selectedClass`;
|
||||||
* all sibling labels lose it.
|
* all sibling labels lose it.
|
||||||
*/
|
*/
|
||||||
export function setupCardRadios(container, selectedClass) {
|
export function setupCardRadios(container, selectedClass, onChange) {
|
||||||
const labels = $qa('label', container);
|
const labels = $qa('label', container);
|
||||||
for (const label of labels) {
|
for (const label of labels) {
|
||||||
const radio = $q('input[type="radio"]', label);
|
const radio = $q('input[type="radio"]', label);
|
||||||
@@ -109,6 +109,7 @@ export function setupCardRadios(container, selectedClass) {
|
|||||||
if ($q('input[type="radio"]', l)) l.classList.remove(selectedClass);
|
if ($q('input[type="radio"]', l)) l.classList.remove(selectedClass);
|
||||||
}
|
}
|
||||||
if (radio.checked) label.classList.add(selectedClass);
|
if (radio.checked) label.classList.add(selectedClass);
|
||||||
|
if (onChange) onChange(radio);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const TL = {
|
|||||||
STATUS: {
|
STATUS: {
|
||||||
DEVICE_RECOGNIZED: 'Your device has been recognized. You can continue to the next step!',
|
DEVICE_RECOGNIZED: 'Your device has been recognized. You can continue to the next step!',
|
||||||
NM_REMOVED_ON_REBOOT: 'NickelMenu will be removed on next reboot.',
|
NM_REMOVED_ON_REBOOT: 'NickelMenu will be removed on next reboot.',
|
||||||
NM_INSTALLED: 'NickelMenu has been prepared for your Kobo. To complete the installation, follow the instructions below.',
|
NM_INSTALLED: 'NickelMenu has been installed on your Kobo. To complete the installation, follow the instructions below.',
|
||||||
NM_DOWNLOAD_READY: 'Your NickelMenu package is ready to download. After downloading, a list of installation steps will be displayed.',
|
NM_DOWNLOAD_READY: 'Your NickelMenu package is ready to download. After downloading, a list of installation steps will be displayed.',
|
||||||
NM_WILL_BE_REMOVED: 'NickelMenu will be updated and marked for removal. It will uninstall itself when your Kobo reboots.',
|
NM_WILL_BE_REMOVED: 'NickelMenu will be updated and marked for removal. It will uninstall itself when your Kobo reboots.',
|
||||||
NM_WILL_BE_INSTALLED: 'The following will be installed on your Kobo:',
|
NM_WILL_BE_INSTALLED: 'The following will be installed on your Kobo:',
|
||||||
|
|||||||
Reference in New Issue
Block a user