Skip to content

Commit 64060ea

Browse files
committed
examples e2e testing - live_w_locator
- live_w_locator several bug fixes, related to rapidly clicking around in places - fix exposure of drawScannerArea in public interface - add test:parallel npm script
1 parent 201fd10 commit 64060ea

6 files changed

Lines changed: 260 additions & 28 deletions

File tree

cypress/e2e/examples.cy.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// E2E tests for example pages
2+
describe('Example Pages E2E', () => {
3+
describe('live_w_locator.html', () => {
4+
beforeEach(() => {
5+
cy.visit('http://localhost:8080/live_w_locator.html');
6+
});
7+
8+
afterEach(() => {
9+
cy.window().then((win: any) => {
10+
if (win.Quagga && win.Quagga.stop) {
11+
const stopPromise = win.Quagga.stop();
12+
if (stopPromise && typeof stopPromise.then === 'function') {
13+
return cy.wrap(stopPromise);
14+
}
15+
}
16+
});
17+
});
18+
19+
after(() => {
20+
cy.window().then((win: any) => {
21+
if (win.Quagga && win.Quagga.stop) {
22+
const stopPromise = win.Quagga.stop();
23+
if (stopPromise && typeof stopPromise.then === 'function') {
24+
return cy.wrap(stopPromise);
25+
}
26+
}
27+
});
28+
});
29+
30+
it('should load the page successfully', () => {
31+
cy.contains('h1', 'QuaggaJS').should('be.visible');
32+
});
33+
34+
it('should have a camera dropdown', () => {
35+
cy.get('select#deviceSelection').should('exist');
36+
});
37+
38+
it('should have a locate checkbox', () => {
39+
cy.get('input[name="locate"]').should('exist');
40+
});
41+
42+
it('should have canvas elements', () => {
43+
// Canvas is created by Quagga inside #interactive div
44+
cy.get('#interactive').should('exist');
45+
});
46+
47+
it('should have Quagga available on window', () => {
48+
cy.window().its('Quagga').should('exist');
49+
});
50+
51+
it('should toggle locate checkbox', () => {
52+
cy.get('input[name="locate"]').uncheck().should('not.be.checked');
53+
cy.get('input[name="locate"]').check().should('be.checked');
54+
});
55+
56+
it('should populate camera dropdown with devices', () => {
57+
// Wait for devices to be enumerated and dropdown populated
58+
cy.get('select#deviceSelection option', { timeout: 10000 })
59+
.should('have.length.at.least', 1);
60+
});
61+
62+
63+
it('provides scanning area box when locate=false', () => {
64+
// Wait for Quagga to be initialized and processing frames before toggling locate
65+
cy.window().then((win: any) => {
66+
expect(win.Quagga).to.exist;
67+
return new Cypress.Promise((resolve) => {
68+
const timeout = setTimeout(() => resolve(null), 1500);
69+
win.Quagga.onProcessed(() => {
70+
clearTimeout(timeout);
71+
resolve(null);
72+
});
73+
});
74+
});
75+
76+
// Now toggle locate off and wait for reinit
77+
cy.get('input[name="locate"]').uncheck().should('not.be.checked').trigger('change');
78+
cy.wait(400); // Allow time for debounced reinit (250ms) + init completion
79+
80+
cy.window().then((win: any) => {
81+
expect(win.Quagga).to.exist;
82+
expect(win.Quagga.onProcessed).to.be.a('function');
83+
84+
const p = new Cypress.Promise((resolve) => {
85+
const timeout = setTimeout(() => resolve(null), 2000);
86+
87+
win.Quagga.onProcessed(function (result: any) {
88+
if (result && (result.box || (result.boxes && result.boxes.length > 0))) {
89+
clearTimeout(timeout);
90+
resolve(result);
91+
}
92+
});
93+
});
94+
return cy.wrap(p, { timeout: 3000 });
95+
}).then((result: any) => {
96+
expect(result, 'Should receive result with box or boxes').to.exist;
97+
// When locate=false, should have either result.box or result.boxes
98+
const hasBox = result.box && Array.isArray(result.box) && result.box.length === 4;
99+
const hasBoxes = result.boxes && Array.isArray(result.boxes) && result.boxes.length > 0;
100+
expect(hasBox || hasBoxes, 'Should have either result.box or result.boxes with scanning area').to.be.true;
101+
});
102+
});
103+
104+
it('continues processing frames after resolution change', () => {
105+
// Select a different resolution option dynamically (choose last option if available)
106+
cy.contains('label', 'Resolution (width)')
107+
.find('select[name="input-stream_constraints"]').then($sel => {
108+
const current = ($sel[0] as HTMLSelectElement).value;
109+
const options = Array.from(($sel[0] as HTMLSelectElement).options).map(o => o.value);
110+
const alt = options.reverse().find(v => v !== current) || current;
111+
cy.wrap($sel).select(alt);
112+
});
113+
// Verify frames continue after resolution change
114+
cy.window().then((win: any) => {
115+
expect(win.Quagga).to.exist;
116+
expect(win.Quagga.onProcessed).to.be.a('function');
117+
let count = 0;
118+
const p = new Cypress.Promise((resolve) => {
119+
const timeout = setTimeout(() => resolve(null), 3000);
120+
win.Quagga.onProcessed(() => {
121+
count++;
122+
if (count >= 3) {
123+
clearTimeout(timeout);
124+
resolve(null);
125+
}
126+
});
127+
});
128+
return cy.wrap(p, { timeout: 3500 }).then(() => {
129+
expect(count).to.be.greaterThan(0);
130+
});
131+
});
132+
});
133+
134+
it('continues processing frames after patch size change', () => {
135+
cy.contains('label', 'Patch-Size')
136+
.find('select[name="locator_patch-size"]').select('large');
137+
cy.window().then((win: any) => {
138+
expect(win.Quagga).to.exist;
139+
expect(win.Quagga.onProcessed).to.be.a('function');
140+
let count = 0;
141+
const p = new Cypress.Promise((resolve) => {
142+
win.Quagga.onProcessed(() => { count++; if (count >= 3) resolve(null); });
143+
});
144+
return cy.wrap(p, { timeout: 3000 }).then(() => {
145+
expect(count).to.be.greaterThan(0);
146+
});
147+
});
148+
});
149+
150+
it('hides zoom and torch controls when unsupported', () => {
151+
// Labels wrap the inputs and are set to display:none when capability missing
152+
cy.get('label:has(select[name="settings_zoom"])').should('have.css', 'display', 'none');
153+
cy.get('label:has(input[name="settings_torch"])').should('have.css', 'display', 'none');
154+
});
155+
});
156+
});

docs/contributing.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,24 @@ merging.
3939
**Running Specific Tests**:
4040

4141
```bash
42-
# All tests (unit + integration, Node + browser)
42+
# All tests (Node unit/integration + full Cypress suite)
4343
npm test
4444

45+
# Run Node and Cypress in parallel (faster local iteration)
46+
npm run test:parallel
47+
4548
# Node tests only
4649
npm run test:node
4750

4851
# Browser tests only (requires Cypress)
4952
npm run test:browser-all
5053

54+
# E2E examples spec with server (headless)
55+
npm run test:e2e
56+
57+
# E2E examples spec (interactive, Electron)
58+
npm run test:e2e:open
59+
5160
# Cypress: run only specific spec(s)
5261
# Pass --spec through npm using --
5362
npm run cypress:run -- --spec cypress/e2e/browser.cy.ts
@@ -61,6 +70,9 @@ npm run cypress:run -- --spec "cypress/e2e/**/browser*.cy.ts"
6170
# Open interactive runner
6271
npm run cypress:open
6372

73+
# Open only the examples E2E spec (Electron)
74+
npm run cypress:open:e2e
75+
6476
# Choose a browser (if installed)
6577
npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress run --browser chrome --env BUILD_ENV=development --spec cypress/e2e/browser.cy.ts
6678

@@ -82,6 +94,11 @@ environments by default. If a test is known to fail in a specific environment, m
8294

8395
See `test/integration/README.md` for complete details on the test failure marking system.
8496

97+
**Notes**:
98+
99+
- The Cypress E2E examples (`cypress/e2e/examples.cy.ts`) require the local examples server on port 8080; the `test`, `test:e2e`, and `test:e2e:open` scripts start it automatically via `start-server-and-test`.
100+
- The parallel runner uses prefixed logs (`e2e`, `node`) and exits non-zero if either side fails (`--kill-others-on-fail --success=all`).
101+
85102
#### Working on a changed copy of Quagga2 from another repository (ie, developing an external plugin) {#working-on-external-plugin}
86103

87104
If you need to make changes to Quagga2 to support some external code (such as an external reader plugin),

docs/examples/live_w_locator.js

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ $(function() {
2222
}
2323
});
2424
var App = {
25+
_initDebounceTimer: null,
26+
_pendingReinit: false,
2527
init: function() {
2628
var self = this;
2729
Quagga.init(this.state, function(err) {
@@ -33,8 +35,24 @@ $(function() {
3335
Quagga.start();
3436
App.initCameraSelection();
3537
App.checkCapabilities();
38+
// Sync UI checkboxes to match actual state after init
39+
$('input[name="locate"]').prop('checked', App.state.locate);
3640
});
3741
},
42+
reinit: function() {
43+
// Debounced reinit: cancel pending and schedule a new one
44+
console.log('[App.reinit] Called, current timer:', App._initDebounceTimer !== null, 'Current state.locate:', App.state.locate);
45+
if (App._initDebounceTimer) {
46+
console.log('[App.reinit] Cancelling pending init');
47+
clearTimeout(App._initDebounceTimer);
48+
}
49+
App._initDebounceTimer = setTimeout(function() {
50+
console.log('[App.reinit] Debounce expired, calling init() with state.locate:', App.state.locate);
51+
App._initDebounceTimer = null;
52+
App._pendingReinit = false;
53+
App.init();
54+
}, 250);
55+
},
3856
handleError: function(err) {
3957
console.log(err);
4058
// If we attempted to open a specific device and failed, revert to last known-good constraints
@@ -134,7 +152,13 @@ $(function() {
134152
attachListeners: function() {
135153
var self = this;
136154

155+
console.log('[App.attachListeners] Called - this should only happen once per init');
137156
self.initCameraSelection();
157+
158+
// Remove any existing handlers to prevent stacking
159+
$(".controls").off("click", "button.stop");
160+
$(".controls .reader-config-group").off("change", "input, select");
161+
138162
$(".controls").on("click", "button.stop", function(e) {
139163
e.preventDefault();
140164
Quagga.stop();
@@ -148,7 +172,7 @@ $(function() {
148172
name = $target.attr("name"),
149173
state = self._convertNameToState(name);
150174

151-
console.log("Value of "+ state + " changed to " + value);
175+
console.log("Value of "+ state + " changed to " + value, "checkbox checked prop:", $target.prop("checked"), "target type:", $target.attr("type"));
152176
self.setState(state, value);
153177
});
154178
},
@@ -203,6 +227,8 @@ $(function() {
203227
setState: function(path, value) {
204228
var self = this;
205229

230+
console.log('[App.setState] ENTRY: path=', path, 'value=', value, 'current state.locate=', self.state.locate);
231+
206232
if (typeof self._accessByPath(self.inputMapper, path) === "function") {
207233
value = self._accessByPath(self.inputMapper, path)(value);
208234
}
@@ -220,32 +246,31 @@ $(function() {
220246
self._accessByPath(self.state, path, value);
221247
}
222248

249+
console.log('[App.setState] AFTER _accessByPath: state.locate=', self.state.locate);
223250
console.log(JSON.stringify(self.state));
224251

225-
// For camera/device changes, perform a clean stop → init → start sequence
226-
var isDeviceSwitch = (path === 'inputStream.constraints' && value && typeof value === 'object' && 'deviceId' in value);
227-
if (isDeviceSwitch) {
228-
try {
229-
// Ensure we don't stack DOM/event listeners across re-inits
230-
App.detachListeners();
231-
var stopResult = Quagga.stop();
232-
// If stop returns a promise (newer builds), wait before re-init
233-
if (stopResult && typeof stopResult.then === 'function') {
234-
stopResult.then(function(){
235-
// Reattach fresh listeners via init
236-
App.init();
237-
});
238-
} else {
239-
App.init();
240-
}
241-
} catch(e) {
242-
// Fallback: attempt reinit anyway
243-
App.init();
244-
}
252+
console.log('[App.setState] path:', path, 'pending:', App._pendingReinit);
253+
// Prevent overlapping stop/reinit sequences
254+
if (App._pendingReinit) {
255+
// Already stopping/reiniting, just update the debounce timer
256+
console.log('[App.setState] Already pending, just updating debounce');
257+
App.reinit();
258+
return;
259+
}
260+
261+
console.log('[App.setState] Calling stop and scheduling reinit');
262+
App._pendingReinit = true;
263+
App.detachListeners();
264+
var stopResult = Quagga.stop();
265+
if (stopResult && typeof stopResult.then === 'function') {
266+
stopResult.then(function(){
267+
console.log('[App.setState] Stop completed (promise), calling reinit');
268+
App.reinit();
269+
});
245270
} else {
246-
App.detachListeners();
247-
Quagga.stop();
248-
App.init();
271+
// Older sync stop; reinit with debounce
272+
console.log('[App.setState] Stop completed (sync), calling reinit');
273+
App.reinit();
249274
}
250275
},
251276
inputMapper: {
@@ -322,9 +347,21 @@ $(function() {
322347
lastResult : null
323348
};
324349

325-
App.init();
350+
console.log('[Startup] Calling initial App.reinit()');
351+
App.reinit();
326352

353+
var processedCount = 0;
327354
Quagga.onProcessed(function(result) {
355+
processedCount++;
356+
/*
357+
console.log('onProcessed #' + processedCount + ':', result ? {
358+
hasBox: !!result.box,
359+
boxLength: result.box ? result.box.length : 0,
360+
hasBoxes: !!result.boxes,
361+
boxesLength: result.boxes ? result.boxes.length : 0,
362+
codeResult: !!result.codeResult
363+
} : 'null result');
364+
*/
328365
var drawingCtx = Quagga.canvas.ctx.overlay,
329366
drawingCanvas = Quagga.canvas.dom.overlay;
330367

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,22 @@
7171
"precoverage": "npx rimraf .nyc_output coverage",
7272
"coverage:node": "// DOES NOT WORK YET // npx cross-env NODE_ENV=test BUILD_ENV=development nyc --no-clean ts-mocha -p test/tsconfig.json src/**/test/node/*.spec.* src/**/test/*.spec.*",
7373
"coverage": "npm run cypress:run",
74-
"cypress:open": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress open --env BUILD_ENV=development",
74+
"server:examples": "npx http-server docs/examples -p 8080",
75+
"cypress:open": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress open --env BUILD_ENV=development --browser electron",
76+
"cypress:open:e2e": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress open --e2e --env BUILD_ENV=development --config specPattern=\"cypress/e2e/examples.cy.ts\" --browser electron",
7577
"cypress:run": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress run --env BUILD_ENV=development",
78+
"cypress:run:e2e": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider cypress run --env BUILD_ENV=development --spec \"cypress/e2e/examples.cy.ts\"",
79+
"test:e2e": "npx start-server-and-test server:examples http://localhost:8080 cypress:run:e2e",
80+
"test:e2e:open": "npx start-server-and-test server:examples http://localhost:8080 cypress:open:e2e",
7681
"test:browser-specific": "NOT WORKING -- something like npx cypress run --config testFiles=[browser]",
7782
"test:browser-universal": "NOT WORKING -- something like npx cypress run --config testFiles=[universal]",
7883
"test:browser-all": "npm run cypress:run",
7984
"test:import": "mocha test/test-import.mjs",
8085
"test:node": "npx cross-env NODE_ENV=test ts-mocha -p test/tsconfig.json src/**/test/node/*.spec.* src/**/test/*.spec.* test/integration/**/*.spec.ts",
8186
"test:require": "mocha test/test-require",
8287
"test:module": "npm run test:require && npm run test:import",
83-
"test": "npm run cypress:run && npx cross-env NODE_ENV=test BUILD_ENV=development ts-mocha -p test/tsconfig.json src/**/test/node/*.spec.* src/**/test/*.spec.* test/integration/**/*.spec.ts",
88+
"test": "npx start-server-and-test server:examples http://localhost:8080 cypress:run && npx cross-env NODE_ENV=test BUILD_ENV=development ts-mocha -p test/tsconfig.json src/**/test/node/*.spec.* src/**/test/*.spec.* test/integration/**/*.spec.ts",
89+
"test:parallel": "npx concurrently --kill-others-on-fail --success=all -n e2e,node -c green,blue \"npx start-server-and-test server:examples http://localhost:8080 cypress:run\" \"npm run test:node\"",
8490
"build-and-test": "npm run build && npm run test",
8591
"build:dev": "npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider webpack --config configs/webpack.config.js",
8692
"build:prod": "npx cross-env NODE_ENV=production BUILD_ENV=production NODE_OPTIONS=--openssl-legacy-provider webpack --config configs/webpack.config.min.js",

src/quagga.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ const QuaggaJSStaticInterface = {
122122
get canvas() {
123123
return _context.canvasContainer;
124124
},
125+
drawScannerArea: function () {
126+
return instance.drawScannerArea();
127+
},
125128
decodeSingle: function (config, resultCallback) {
126129
const quaggaInstance = new Quagga();
127130
config = merge({

0 commit comments

Comments
 (0)