diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..5f7928d6a
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,72 @@
+version: 2.1
+jobs:
+ test:
+ docker:
+ - image: mcr.microsoft.com/playwright:v1.43.1-jammy
+ steps:
+ - checkout
+
+ - run:
+ name: Install system dependencies
+ command: apt-get update && apt-get install -y curl
+
+ - run:
+ name: Install dependencies
+ command: npm ci
+
+ - run:
+ name: Install Playwright Browsers
+ command: npx playwright install --with-deps
+
+ - run:
+ name: Start Grunt Server
+ background: true
+ command: |
+ npx grunt connect:livereload:keepalive
+
+ - run:
+ name: Wait for server to be ready
+ command: |
+ echo "Waiting for server to start..."
+ # Give the server a moment to initialize
+ sleep 3
+
+ for i in {1..30}; do
+ if curl -sSf http://localhost:8000 >/dev/null 2>&1; then
+ echo "Server is ready on port 8000!"
+ # Verify it's actually serving content
+ curl -s http://localhost:8000 | head -5
+ exit 0
+ fi
+ echo "Attempt $i/30: Server not ready yet..."
+ sleep 2
+ done
+
+ echo "Server failed to start after 60 seconds"
+ echo "Debug information:"
+ netstat -tlnp | grep :8000 || echo "No process listening on port 8000"
+ ps aux | grep grunt || echo "No grunt processes found"
+
+ # Check if grunt process exists but maybe on different port
+ ss -tlnp | grep :8000 || echo "No process listening on port 8000"
+ ps aux | grep grunt || echo "No grunt processes found"
+
+ # Check if grunt process exists but maybe on different port
+ ss -tlnp | grep grunt || echo "No grunt-related ports found"
+
+ exit 1
+
+ - run:
+ name: Run Playwright tests
+ command: npx playwright test
+ no_output_timeout: 10m
+
+ - store_artifacts:
+ path: test-results/
+ destination: playwright-test-results
+
+workflows:
+ version: 2
+ test:
+ jobs:
+ - test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c3d69bf16..dc2cceb31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,10 @@ build/
app/output/props/*
tempZipDir
.idea/*
-node_modules
\ No newline at end of file
+node_modules
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/Gruntfile.js b/Gruntfile.js
index 5926d4ecb..8be3bc014 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -200,7 +200,7 @@ module.exports = function (grunt) {
},
macGenConvert: {
cmd: function(str, formDefFile) {
- return 'node macGenConverter.js ' + str + ' > ' + formDefFile;
+ return 'node macGenConverter.js ' + str + ' > ' + formDefFile;
}
}
},
@@ -230,7 +230,7 @@ module.exports = function (grunt) {
connect: {
server: {
options: {
- setHeaders: setHeaders
+ setHeaders: setHeaders
}
},
options: {
@@ -247,10 +247,10 @@ module.exports = function (grunt) {
next();
});
- middlewares.unshift(postHandler);
- middlewares.unshift(lrSnippet);
middlewares.unshift(mountFolder(baseDirForServer));
middlewares.unshift(mountDirectory(baseDirForServer));
+ middlewares.unshift(postHandler);
+ middlewares.unshift(lrSnippet);
return middlewares;
}
}
@@ -259,33 +259,37 @@ module.exports = function (grunt) {
options: {
port: 8001,
middleware: [
- postHandler,
- lrSnippet,
- mountFolder('test'),
- mountFolder(baseDirForServer),
- mountDirectory(baseDirForServer)
- ]
- }
+ mountFolder('test'),
+ mountFolder(baseDirForServer),
+ mountDirectory(baseDirForServer),
+ postHandler,
+ lrSnippet
+ ]
+ }
}
},
- open: {
+ open: {
server: {
path: 'http://localhost:<%= connect.options.port %>/index.html',
- app: {app: (function() {
- var platform = require('os').platform();
- // windows: *win*
- // mac: darwin
- if (platform.search('win') >= 0 &&
- platform.search('darwin') < 0) {
- // Windows expects chrome.
- grunt.log.writeln('detected Windows environment');
- return 'chrome';
- } else {
- // Mac (and maybe others--add as discovered), expects
- // Google Chrome
- grunt.log.writeln('detected non-Windows environment');
- return 'Google Chrome';
- }
+ app: {
+ app: (function() {
+ var platform = require('os').platform();
+ // windows: *win*
+ // mac: darwin
+ // linux: linux
+ if (platform.search('win') !== -1) {
+ // Windows expects chrome.
+ grunt.log.writeln('Detected Windows environment');
+ return 'chrome';
+ } else if (platform.search('darwin') !== -1) {
+ // Mac expects "Google Chrome"
+ grunt.log.writeln('Detected macOS environment');
+ return 'Google Chrome';
+ } else {
+ // Default for Linux and potentially other environments
+ grunt.log.writeln('Detected non-Windows, non-macOS environment');
+ return 'google-chrome';
+ }
})()
}
}
@@ -365,7 +369,7 @@ module.exports = function (grunt) {
var platform = require('os').platform();
var isWindows = (platform.search('win') >= 0 &&
platform.search('darwin') < 0);
-
+
var dirs = grunt.file.expand(
{filter: function(path) {
if ( !path.endsWith(".xlsx") ) {
@@ -373,7 +377,7 @@ module.exports = function (grunt) {
}
var cells = path.split((isWindows ? "\\" : "/"));
return (cells.length >= 6) &&
- ( cells[cells.length-1] === cells[cells.length-2] + ".xlsx" );
+ ( cells[cells.length-1] === cells[cells.length-2] + ".xlsx" );
},
cwd: 'app' },
'**/*.xlsx',
@@ -396,30 +400,30 @@ module.exports = function (grunt) {
});
var zipAllFiles = function( destZipFile, filesList, completionFn ) {
- // create a file to stream archive data to.
+ // create a file to stream archive data to.
var fs = require('fs');
var archiver = require('archiver');
var output = fs.createWriteStream(destZipFile);
var archive = archiver('zip', {
- store: true // Sets the compression method to STORE.
+ store: true // Sets the compression method to STORE.
});
-
- // listen for all archive data to be written
+
+ // listen for all archive data to be written
output.on('close', function() {
console.log(archive.pointer() + ' total bytes');
console.log('archiver has been finalized and the output file descriptor has closed.');
completionFn(true);
});
-
- // good practice to catch this error explicitly
+
+ // good practice to catch this error explicitly
archive.on('error', function(err) {
throw err;
});
-
- // pipe archive data to the file
+
+ // pipe archive data to the file
archive.pipe(output);
-
+
filesList.forEach(function(fileName) {
// Have to add app back into the file name for the adb push
var src = tablesConfig.appDir + '/' + fileName;
@@ -429,7 +433,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
grunt.log.writeln('error ' + err + ' adding ' + src + ' to file ' + destZipFile);
}} );
});
- // finalize the archive (ie we are done appending files but streams have to finish yet)
+ // finalize the archive (ie we are done appending files but streams have to finish yet)
archive.finalize();
};
@@ -438,12 +442,12 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'BROKEN: does not compress and last file is not terminated properly. Construct the configzip and systemzip for survey and tables',
function() {
var done = this.async();
-
+
var buildDir = 'build' +
'/zips';
-
+
grunt.file.delete(buildDir + '/');
-
+
grunt.file.mkdir(buildDir);
grunt.file.mkdir(buildDir + '/survey/');
grunt.file.mkdir(buildDir + '/tables/');
@@ -465,7 +469,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/framework/**',
'config/assets/commonDefinitions.js',
'config/assets/img/play.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/backup.png',
'config/assets/img/advance.png',
'config/assets/css/odk-survey.css',
@@ -493,16 +497,16 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'!**/.DS_Store',
'!**/~$*.xlsx');
- zipAllFiles(buildDir + '/survey/systemzip', surveySystemZipFiles,
+ zipAllFiles(buildDir + '/survey/systemzip', surveySystemZipFiles,
function(outcome) {
if ( outcome ) {
- zipAllFiles(buildDir + '/survey/configzip', surveyConfigZipFiles,
+ zipAllFiles(buildDir + '/survey/configzip', surveyConfigZipFiles,
function(outcome) {
if ( outcome ) {
- zipAllFiles(buildDir + '/tables/systemzip', tablesSystemZipFiles,
+ zipAllFiles(buildDir + '/tables/systemzip', tablesSystemZipFiles,
function(outcome) {
if ( outcome ) {
- zipAllFiles(buildDir + '/tables/configzip', tablesConfigZipFiles,
+ zipAllFiles(buildDir + '/tables/configzip', tablesConfigZipFiles,
function(outcome) {
if ( outcome ) {
grunt.log.writeln('success!');
@@ -816,7 +820,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
destFileName = destFileName.substring(0,idx) + destFileName.substring(idx+demoInfix.length);
idx = destFileName.indexOf(demoInfix + ".");
}
-
+
var idxDir = destFileName.indexOf(demoInfix + "/");
while ( idxDir >= 0 ) {
// directory...
@@ -824,7 +828,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
destFileName = destFileName.substring(0,idxDir) + destFileName.substring(idxDir+demoInfix.length);
idxDir = destFileName.indexOf(demoInfix + "/");
}
-
+
var buildDir = 'build' +
'/' +
demoInfix.substring(1);
@@ -858,7 +862,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
//
// returns a function that will handle the adb push of files onto the
- // device with any files containing ".infix." stripped of that infix
+ // device with any files containing ".infix." stripped of that infix
// and any folders ending in ".infix" also stripped.
//
var infixRenameAdbPusher = function(demoInfix, offsetDir) {
@@ -884,7 +888,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
destFileName = destFileName.substring(0,idx) + destFileName.substring(idx+demoInfix.length);
idx = destFileName.indexOf(demoInfix + ".");
}
-
+
var idxDir = destFileName.indexOf(demoInfix + "/");
while ( idxDir >= 0 ) {
// directory...
@@ -1063,7 +1067,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/css/odk-survey.css',
'config/assets/img/advance.png',
'config/assets/img/backup.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/little_arrow.png',
'config/assets/img/play.png',
'config/assets/libs/**',
@@ -1137,7 +1141,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/css/odk-survey.css',
'config/assets/img/advance.png',
'config/assets/img/backup.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/little_arrow.png',
'config/assets/img/play.png',
'config/assets/libs/**',
@@ -1216,7 +1220,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/css/odk-survey.css',
'config/assets/img/advance.png',
'config/assets/img/backup.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/little_arrow.png',
'config/assets/img/play.png',
'config/assets/libs/**',
@@ -1306,7 +1310,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/css/odk-survey.css',
'config/assets/img/advance.png',
'config/assets/img/backup.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/little_arrow.png',
'config/assets/img/play.png',
'config/assets/libs/**',
@@ -1468,7 +1472,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
'config/assets/css/odk-survey.css',
'config/assets/img/advance.png',
'config/assets/img/backup.png',
- 'config/assets/img/form_logo.png',
+ 'config/assets/img/form_logo_new.png',
'config/assets/img/little_arrow.png',
'config/assets/img/play.png',
'config/assets/libs/**',
@@ -1858,7 +1862,7 @@ var zipAllFiles = function( destZipFile, filesList, completionFn ) {
// to /sdcard/odk/forms
// I.e., this folder should contain things like:
// .../formid.xml
- // .../formid-media/form_logo.jpg
+ // .../formid-media/form_logo_new.jpg
// .../formid-media/...
// .../formid2.xml
//
diff --git a/app/config/assets/css/odk-survey.css b/app/config/assets/css/odk-survey.css
index 955a4e21c..379710400 100644
--- a/app/config/assets/css/odk-survey.css
+++ b/app/config/assets/css/odk-survey.css
@@ -381,8 +381,10 @@ Eventually we should make a jqm theme for this.
}
.odk-formlogo {
- width: 100%;
+ width: 40%;
+ height: 40%;
}
+
.odk-label-image {
width: 100%;
}
@@ -679,3 +681,4 @@ Eventually we should make a jqm theme for this.
overflow:hidden;
margin-right:50px;
}
+
diff --git a/app/config/assets/framework/frameworkDefinitions.js b/app/config/assets/framework/frameworkDefinitions.js
index e7f777d29..b82579a36 100644
--- a/app/config/assets/framework/frameworkDefinitions.js
+++ b/app/config/assets/framework/frameworkDefinitions.js
@@ -215,8 +215,8 @@ window.odkFrameworkDefinitions = {
"survey_form_identification": {
"string_token": "survey_form_identification",
"text": {
- "default": "
ODK Survey
Form name: {{localizeText form_title}}
{{#if form_version}}
Form version: {{form_version}}
{{/if}}
",
- "es": "ODK Survey
Nombre del Formulario: {{localizeText form_title}}
{{#if form_version}}
Version de Formulario: {{form_version}}
{{/if}}
"
+ "default": "SURVEY
Form name: {{localizeText form_title}}
{{#if form_version}}
Form version: {{form_version}}
{{/if}}
",
+ "es": "SURVEY
Nombre del Formulario: {{localizeText form_title}}
{{#if form_version}}
Version de Formulario: {{form_version}}
{{/if}}
"
},
"_row_num": 28
},
diff --git a/app/config/assets/img/form_logo_new.png b/app/config/assets/img/form_logo_new.png
new file mode 100644
index 000000000..2f604a2f4
Binary files /dev/null and b/app/config/assets/img/form_logo_new.png differ
diff --git a/app/system/survey/js/prompts.js b/app/system/survey/js/prompts.js
index 32708fb00..b4710dc67 100644
--- a/app/system/survey/js/prompts.js
+++ b/app/system/survey/js/prompts.js
@@ -223,10 +223,10 @@ promptTypes.base = Backbone.View.extend({
if (currEl.length > 0) {
currElString = currEl[0].innerHTML;
currElStringNoSpaces = currElString.replace(/\s/g, '');
- }
+ }
var toBeDrawnEl = that.template(that.renderContext, {
// Subverting breaking change in handlebars v. 4.6 to allow access to "not own" properties (insecure)
- allowProtoMethodsByDefault: true,
+ allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: true
});
var tbdString = null;
@@ -473,7 +473,7 @@ promptTypes.opening = promptTypes.base.extend({
ctxt.success();
},
renderContext: {
- headerImg: requirejs.toUrl('../config/assets/img/form_logo.png'),
+ headerImg: requirejs.toUrl('../config/assets/img/form_logo_new.png'),
backupImg: requirejs.toUrl('../config/assets/img/backup.png'),
advanceImg: requirejs.toUrl('../config/assets/img/advance.png')
},
@@ -641,7 +641,7 @@ promptTypes.instances = promptTypes.base.extend({
});
$.extend(that.renderContext, {
- headerImg: requirejs.toUrl('../config/assets/img/form_logo.png')
+ headerImg: requirejs.toUrl('../config/assets/img/form_logo_new.png')
});
if ( that._screen && that._screen._renderContext ) {
that._screen._renderContext.showHeader = false;
@@ -2209,7 +2209,7 @@ promptTypes.birth_date = promptTypes.date_no_time.extend({
}
},
});
-
+
promptTypes.date_year_only = promptTypes.date_no_time.extend({
type: "date",
showTime: false,
@@ -2336,7 +2336,7 @@ promptTypes.coptic_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2366,7 +2366,7 @@ promptTypes.ethiopian_calendar_picker = promptTypes.non_gregorian_calendar.exten
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2396,7 +2396,7 @@ promptTypes.hebrew_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2426,7 +2426,7 @@ promptTypes.islamic_calendar_picker = promptTypes.non_gregorian_calendar.extend(
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2456,7 +2456,7 @@ promptTypes.julian_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2486,7 +2486,7 @@ promptTypes.mayan_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2516,7 +2516,7 @@ promptTypes.nanakshahi_calendar_picker = promptTypes.non_gregorian_calendar.exte
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2546,7 +2546,7 @@ promptTypes.nepali_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2576,7 +2576,7 @@ promptTypes.persian_calendar_picker = promptTypes.non_gregorian_calendar.extend(
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2606,7 +2606,7 @@ promptTypes.taiwan_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2636,7 +2636,7 @@ promptTypes.thai_calendar_picker = promptTypes.non_gregorian_calendar.extend({
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
@@ -2666,7 +2666,7 @@ promptTypes.ummalqura_calendar_picker = promptTypes.non_gregorian_calendar.exten
that.insideAfterRender = false;
}
},
-
+
formatDBVal: function(formattedDateValue) {
var outputValue = null;
if (formattedDateValue !== null && formattedDateValue !== undefined && !(_.isEmpty(formattedDateValue))) {
diff --git a/app/system/survey/templates/finalize.handlebars b/app/system/survey/templates/finalize.handlebars
index 9ac611edf..3b478d121 100644
--- a/app/system/survey/templates/finalize.handlebars
+++ b/app/system/survey/templates/finalize.handlebars
@@ -1,7 +1,6 @@
{{#if headerImg}}
-

-
+{{!-- logo removed--}}
{{/if}}
{{{localizeText display.finalize_survey_form_identification}}}
@@ -13,7 +12,7 @@
-
+
diff --git a/app/system/survey/templates/instances.handlebars b/app/system/survey/templates/instances.handlebars
index 1b70e08c3..6bdc43c7c 100644
--- a/app/system/survey/templates/instances.handlebars
+++ b/app/system/survey/templates/instances.handlebars
@@ -1,62 +1,59 @@
-
-
-

-
-
- {{{localizeText display.instances_survey_form_identification}}}
-
-
-
-
-
-
-
- {{{localizeText display.instances_previously_created_instances_label}}}
-
-
-
- {{#each instances}}
- {{#if dateDivider}}
- - {{dateDivider}}
- {{/if}}
- -
-
-
- {{else}}
- - {{localizeText display.instances_no_saved_instances_label}}
- {{/each}}
-
-
-
+
+{{!-- logo removed--}}
+ {{{localizeText display.instances_survey_form_identification}}}
+
+
+
+
+
+
+
+ {{{localizeText display.instances_previously_created_instances_label}}}
+
+
+
+ {{#each instances}}
+ {{#if dateDivider}}
+ - {{dateDivider}}
+ {{/if}}
+ -
+
+
+ {{else}}
+ - {{localizeText display.instances_no_saved_instances_label}}
+ {{/each}}
+
+
+
diff --git a/app/system/survey/templates/opening.handlebars b/app/system/survey/templates/opening.handlebars
index 2490feacc..e24250da3 100644
--- a/app/system/survey/templates/opening.handlebars
+++ b/app/system/survey/templates/opening.handlebars
@@ -1,7 +1,5 @@
-
-

-
+{{!-- removed logo --}}
{{{localizeText display.opening_survey_form_identification}}}
@@ -17,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/package-lock.json b/package-lock.json
index cbca4ed14..2a4000b2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,21 +15,39 @@
"mkdirp": "^1.0.4"
},
"devDependencies": {
+ "@playwright/test": "^1.53.1",
+ "@types/node": "^24.0.7",
"connect-livereload": "~0.6.1",
- "grunt": "^1.5.3",
- "grunt-contrib-connect": "~4.0.0",
+ "grunt": "^1.6.1",
+ "grunt-contrib-connect": "~5.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-exec": "~3.0.0",
"grunt-open": "~0.2.4",
"load-grunt-tasks": "~5.1.0",
"serve-index": "^1.9.1",
- "serve-static": "^1.15.0",
+ "serve-static": "^1.16.0",
"time-grunt": "~2.0.0"
},
"engines": {
"node": ">=0.8.0"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
+ "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.53.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@sqlite.org/sqlite-wasm": {
"version": "3.45.3-build1",
"resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.45.3-build1.tgz",
@@ -44,6 +62,16 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
+ "node_modules/@types/node": {
+ "version": "24.0.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz",
+ "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -197,25 +225,11 @@
"node": ">=8"
}
},
- "node_modules/assert": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
- "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
- "dev": true,
- "dependencies": {
- "util": "0.10.3"
- }
- },
"node_modules/async": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
- "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
- },
- "node_modules/async-limiter": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
- "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
- "dev": true
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.0",
@@ -309,12 +323,13 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -541,9 +556,9 @@
}
},
"node_modules/dateformat": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
- "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+ "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"engines": {
"node": "*"
@@ -581,6 +596,7 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
@@ -595,18 +611,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/duplexify": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
- "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
- "dev": true,
- "dependencies": {
- "end-of-stream": "^1.0.0",
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.0",
- "stream-shift": "^1.0.0"
- }
- },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -672,6 +676,7 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -682,15 +687,6 @@
"integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
"dev": true
},
- "node_modules/events": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
- "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
- "dev": true,
- "engines": {
- "node": ">=0.4.x"
- }
- },
"node_modules/exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -752,10 +748,11 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -782,31 +779,18 @@
}
},
"node_modules/findup-sync": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
- "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
- "dev": true,
- "dependencies": {
- "glob": "~5.0.0"
- },
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/findup-sync/node_modules/glob": {
- "version": "5.0.15",
- "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
- "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz",
+ "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==",
"dev": true,
"dependencies": {
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "2 || 3",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.4",
+ "resolve-dir": "^1.0.1"
},
"engines": {
- "node": "*"
+ "node": ">= 10.13.0"
}
},
"node_modules/fined": {
@@ -860,6 +844,7 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -874,6 +859,21 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -978,26 +978,6 @@
"node": ">= 0.10"
}
},
- "node_modules/globule/node_modules/glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/globule/node_modules/minimatch": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
@@ -1016,49 +996,48 @@
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"node_modules/grunt": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz",
- "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz",
+ "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
"dev": true,
"dependencies": {
- "dateformat": "~3.0.3",
+ "dateformat": "~4.6.2",
"eventemitter2": "~0.4.13",
"exit": "~0.1.2",
- "findup-sync": "~0.3.0",
+ "findup-sync": "~5.0.0",
"glob": "~7.1.6",
"grunt-cli": "~1.4.3",
"grunt-known-options": "~2.0.0",
"grunt-legacy-log": "~3.0.0",
"grunt-legacy-util": "~2.0.1",
- "iconv-lite": "~0.4.13",
+ "iconv-lite": "~0.6.3",
"js-yaml": "~3.14.0",
"minimatch": "~3.0.4",
- "mkdirp": "~1.0.4",
- "nopt": "~3.0.6",
- "rimraf": "~3.0.2"
+ "nopt": "~3.0.6"
},
"bin": {
"grunt": "bin/grunt"
},
"engines": {
- "node": ">=8"
+ "node": ">=16"
}
},
"node_modules/grunt-contrib-connect": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/grunt-contrib-connect/-/grunt-contrib-connect-4.0.0.tgz",
- "integrity": "sha512-VR2/+ailwTClAXrvI7bK78roCZzfY1C48vmpdRldohx8P1VXcb51NmBNhukBvG2RKFChNheEcKEcM+wSb/5nYA==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-connect/-/grunt-contrib-connect-5.0.1.tgz",
+ "integrity": "sha512-Hfq/0QJl3ddD2N/a/1cDJHkKEOGk6m7W6uxNe0AmYwtf6v0F/4+8q9rvPJ1tl+mrI90lU/89I9T/h48qqeMfQA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "async": "^3.2.0",
+ "async": "^3.2.5",
"connect": "^3.7.0",
"connect-livereload": "^0.6.1",
+ "http2-wrapper": "^2.2.1",
"morgan": "^1.10.0",
- "node-http2": "^4.0.1",
"open": "^8.0.0",
"portscanner": "^2.2.0",
"serve-index": "^1.9.1",
- "serve-static": "^1.14.1"
+ "serve-static": "^1.15.0"
},
"engines": {
"node": ">=16"
@@ -1322,19 +1301,27 @@
"integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=",
"dev": true
},
- "node_modules/https-browserify": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz",
- "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=",
- "dev": true
+ "node_modules/http2-wrapper": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
+ "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
},
"node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -1463,6 +1450,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -1722,12 +1710,13 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -1739,6 +1728,7 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
+ "license": "MIT",
"bin": {
"mime": "cli.js"
},
@@ -1845,25 +1835,6 @@
"node": ">= 0.6"
}
},
- "node_modules/node-http2": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/node-http2/-/node-http2-4.0.1.tgz",
- "integrity": "sha1-Fk/1O13SLITwrxQrh3xerraAmVk=",
- "dev": true,
- "dependencies": {
- "assert": "1.4.1",
- "events": "1.1.1",
- "https-browserify": "0.0.1",
- "setimmediate": "^1.0.5",
- "stream-browserify": "2.0.1",
- "timers-browserify": "2.0.2",
- "url": "^0.11.0",
- "websocket-stream": "^5.0.1"
- },
- "engines": {
- "node": ">=0.12.0"
- }
- },
"node_modules/nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@@ -2194,6 +2165,38 @@
"node": ">=6"
}
},
+ "node_modules/playwright": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
+ "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.53.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
+ "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/plur": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
@@ -2256,12 +2259,6 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
- "node_modules/punycode": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
- "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
- "dev": true
- },
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@@ -2277,14 +2274,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/querystring": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
- "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
- "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=0.4.x"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": {
@@ -2292,6 +2292,7 @@
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -2366,6 +2367,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve-dir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
@@ -2400,21 +2408,6 @@
"node": ">=8"
}
},
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -2433,10 +2426,11 @@
"dev": true
},
"node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -2461,6 +2455,7 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -2470,6 +2465,7 @@
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
@@ -2485,13 +2481,15 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/send/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
@@ -2503,13 +2501,15 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/send/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -2533,25 +2533,30 @@
}
},
"node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
- "send": "0.18.0"
+ "send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
- "node_modules/setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
- "dev": true
+ "node_modules/serve-static/node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
},
"node_modules/setprototypeof": {
"version": "1.1.0",
@@ -2588,22 +2593,6 @@
"node": ">= 0.6"
}
},
- "node_modules/stream-browserify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
- "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
- "dev": true,
- "dependencies": {
- "inherits": "~2.0.1",
- "readable-stream": "^2.0.2"
- }
- },
- "node_modules/stream-shift": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
- "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
- "dev": true
- },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -2750,18 +2739,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/timers-browserify": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.2.tgz",
- "integrity": "sha1-q0iDz1l9zVCvIRNJoA+8pWrIa4Y=",
- "dev": true,
- "dependencies": {
- "setimmediate": "^1.0.4"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
"node_modules/tiny-lr": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz",
@@ -2796,6 +2773,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -2808,16 +2786,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.6"
}
},
- "node_modules/ultron": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
- "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
- "dev": true
- },
"node_modules/unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
@@ -2840,6 +2813,13 @@
"node": "*"
}
},
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -2849,36 +2829,11 @@
"node": ">= 0.8"
}
},
- "node_modules/url": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
- "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
- "dev": true,
- "dependencies": {
- "punycode": "1.3.2",
- "querystring": "0.2.0"
- }
- },
- "node_modules/util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
- "dev": true,
- "dependencies": {
- "inherits": "2.0.1"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
- "node_modules/util/node_modules/inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
- "dev": true
- },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -2923,20 +2878,6 @@
"node": ">=0.8.0"
}
},
- "node_modules/websocket-stream": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.2.tgz",
- "integrity": "sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==",
- "dev": true,
- "dependencies": {
- "duplexify": "^3.5.1",
- "inherits": "^2.0.1",
- "readable-stream": "^2.3.3",
- "safe-buffer": "^5.1.2",
- "ws": "^3.2.0",
- "xtend": "^4.0.0"
- }
- },
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -2954,26 +2895,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
- "node_modules/ws": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
- "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
- "dev": true,
- "dependencies": {
- "async-limiter": "~1.0.0",
- "safe-buffer": "~5.1.0",
- "ultron": "~1.1.0"
- }
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "dev": true,
- "engines": {
- "node": ">=0.4"
- }
- },
"node_modules/zip-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
diff --git a/package.json b/package.json
index 9003cf091..2c72cce7b 100644
--- a/package.json
+++ b/package.json
@@ -9,15 +9,17 @@
"mkdirp": "^1.0.4"
},
"devDependencies": {
+ "@playwright/test": "^1.53.1",
+ "@types/node": "^24.0.7",
"connect-livereload": "~0.6.1",
- "grunt": "^1.5.3",
- "grunt-contrib-connect": "~4.0.0",
+ "grunt": "^1.6.1",
+ "grunt-contrib-connect": "~5.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-exec": "~3.0.0",
"grunt-open": "~0.2.4",
"load-grunt-tasks": "~5.1.0",
"serve-index": "^1.9.1",
- "serve-static": "^1.15.0",
+ "serve-static": "^1.16.0",
"time-grunt": "~2.0.0"
},
"engines": {
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 000000000..6a3fefcf1
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,81 @@
+// @ts-check
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// import path from 'path';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // url: 'http://localhost:3000',
+ // reuseExistingServer: !process.env.CI,
+ // },
+});
+
diff --git a/tests/assets/census.xlsx b/tests/assets/census.xlsx
new file mode 100644
index 000000000..f925eb425
Binary files /dev/null and b/tests/assets/census.xlsx differ
diff --git a/tests/datetimepicker.spec.js b/tests/datetimepicker.spec.js
new file mode 100644
index 000000000..af7afc369
--- /dev/null
+++ b/tests/datetimepicker.spec.js
@@ -0,0 +1,83 @@
+const { test, expect } = require('@playwright/test');
+
+const BASE_URL = 'http://localhost:8000/index.html';
+const DEFAULT_TIMEOUT = 30000;
+
+const TEST_YEAR = new Date().getFullYear().toString();
+const JANUARY = '0';
+const AUGUST = '7';
+const FIRST_DAY = '1';
+const HOUR_23 = '23';
+const ETHIOPIAN_DAY_7 = '7';
+const ISLAMIC_DAY_9 = '9';
+
+async function getTab1Frame(page) {
+ return await page.locator('#tab1').contentFrame();
+}
+
+async function getScreenFrame(page) {
+ const tab1Frame = await getTab1Frame(page);
+ return await tab1Frame.locator('iframe[name="screen"]').contentFrame();
+}
+
+async function getPreviewScreenFrame(page) {
+ const tab1Frame = await getTab1Frame(page);
+ return await tab1Frame.locator('#previewscreen').contentFrame();
+}
+
+async function clickNext(frame) {
+ await frame.getByText('Next').first().click();
+}
+
+async function selectDateTime(frame, year, month, day, hour = null, minute = null) {
+ await frame.getByRole('combobox').first().selectOption(year);
+ await frame.getByRole('combobox').nth(1).selectOption(month);
+ await frame.getByRole('combobox').nth(2).selectOption(day);
+
+ if (hour !== null) {
+ await frame.getByRole('combobox').nth(3).selectOption(hour);
+ }
+
+ if (minute !== null) {
+ await frame.getByRole('combobox').nth(4).selectOption(minute);
+ }
+}
+
+test('DateTime Picker Flow', async ({ page }) => {
+ page.setDefaultTimeout(DEFAULT_TIMEOUT);
+ await page.goto(BASE_URL, { waitUntil: 'networkidle' });
+
+ const screenFrame = await getScreenFrame(page);
+ const previewScreenFrame = await getPreviewScreenFrame(page);
+
+ await screenFrame.getByRole('link', { name: 'DateTime Picker ' }).click();
+ await screenFrame.getByRole('button', { name: 'Follow link' }).click();
+
+ await previewScreenFrame.getByRole('button', { name: 'Create new instance ' }).click();
+ await clickNext(previewScreenFrame);
+
+ await selectDateTime(previewScreenFrame, TEST_YEAR, JANUARY, FIRST_DAY);
+ await clickNext(previewScreenFrame);
+
+ await selectDateTime(previewScreenFrame, TEST_YEAR, AUGUST, FIRST_DAY, FIRST_DAY, JANUARY);
+ await clickNext(previewScreenFrame);
+
+ await selectDateTime(previewScreenFrame, TEST_YEAR, AUGUST, HOUR_23);
+ await clickNext(previewScreenFrame);
+
+ await previewScreenFrame.getByRole('combobox').first().selectOption(JANUARY);
+ await previewScreenFrame.getByRole('combobox').nth(1).selectOption(JANUARY);
+ await clickNext(previewScreenFrame);
+
+ await previewScreenFrame.getByRole('textbox', { name: 'Ethiopian Calendar' }).click();
+ await previewScreenFrame.getByRole('link', { name: ETHIOPIAN_DAY_7, exact: true }).click();
+ await clickNext(previewScreenFrame);
+
+ await previewScreenFrame.getByRole('textbox', { name: 'Islamic Calendar' }).click();
+ await previewScreenFrame.getByRole('link', { name: ISLAMIC_DAY_9, exact: true }).click();
+ await clickNext(previewScreenFrame);
+
+ await previewScreenFrame.getByRole('button', { name: 'Finalize' }).click();
+
+ await page.waitForTimeout(2000);
+});
diff --git a/tests/surveyForm.spec.js b/tests/surveyForm.spec.js
new file mode 100644
index 000000000..cdae73a69
--- /dev/null
+++ b/tests/surveyForm.spec.js
@@ -0,0 +1,312 @@
+const { test, expect } = require('@playwright/test');
+
+// Constants for URLs and selectors
+const CONSTANTS = {
+ BASE_URL: 'http://localhost:8000/index.html',
+
+ // Frame selectors
+ SELECTORS: {
+ MAIN_TAB: '#tab1',
+ PREVIEW_SCREEN: '#previewscreen',
+ NAVIGATION_FRAME: 'iframe[name="screen"]',
+ NEXT_BUTTON: '.odk-next-btn',
+ },
+
+ // Form names
+ FORMS: {
+ EXAMPLE: 'exampleForm',
+ AGRICULTURE: 'Agriculture',
+ },
+
+ // Button text
+ BUTTONS: {
+ FOLLOW_LINK: 'Follow link',
+ CREATE_NEW_INSTANCE: 'Create new instance ',
+ NEXT: 'Next',
+ FINALIZE: 'Finalize',
+ },
+
+ // Field labels for Example Form
+ EXAMPLE_FORM_FIELDS: {
+ INITIAL_RATING: 'Enter an initial rating (1-10',
+ COMPUTED_ASSIGNMENT: 'computed assignment of',
+ COFFEE_CUPS: 'On average, how many cups of',
+ },
+
+ // Field labels for Agriculture Form
+ AGRICULTURE_FORM_FIELDS: {
+ CORN_VARIETY: 'Enter the name of the corn',
+ PLANT_HEIGHT: 'Enter the height of the plant',
+ },
+
+ // Soil condition options
+ SOIL_CONDITIONS: {
+ DRY: 'Dry',
+ WET: 'Wet',
+ HUMID: 'Humid',
+ },
+
+ // Test data ranges
+ RANGES: {
+ RATING: { MIN: 1, MAX: 10 },
+ COFFEE_CUPS: { MIN: 1, MAX: 8 },
+ PLANT_HEIGHT: { MIN: 50, MAX: 150 },
+ },
+
+ // URL patterns for assertions
+ URL_PATTERNS: {
+ LOCALHOST: /.*localhost:8000.*/,
+ }
+};
+
+class ODKFormPage {
+ constructor(page) {
+ this.page = page;
+ this.baseURL = CONSTANTS.BASE_URL;
+ }
+
+ // Returns the main tab frame containing the form UI
+ async getMainFrame() {
+ const mainFrameLocator = this.page.locator(CONSTANTS.SELECTORS.MAIN_TAB);
+ await mainFrameLocator.waitFor({ state: 'attached', timeout: 10000 });
+ return await mainFrameLocator.contentFrame();
+ }
+
+ // Returns the inner screen frame showing the form fields
+ async getScreenFrame() {
+ const mainFrame = await this.getMainFrame();
+ const screenFrameLocator = mainFrame.locator(CONSTANTS.SELECTORS.PREVIEW_SCREEN);
+ await screenFrameLocator.waitFor({ state: 'attached', timeout: 10000 });
+ return await screenFrameLocator.contentFrame();
+ }
+
+ // Returns the navigation frame containing form links
+ async getNavigationFrame() {
+ const mainFrame = await this.getMainFrame();
+
+ // Get all screen frames
+ const screenFrames = await mainFrame.locator(CONSTANTS.SELECTORS.NAVIGATION_FRAME).all();
+
+ // Try each frame to find the one with navigation links
+ for (const frame of screenFrames) {
+ try {
+ const frameId = await frame.getAttribute('id');
+ console.log(`Checking frame with id: ${frameId}`);
+
+ const contentFrame = await frame.contentFrame();
+
+ // Test if this frame has navigation links by looking for any link
+ const links = await contentFrame.locator('a[role="link"], a').count();
+ console.log(`Frame ${frameId} has ${links} links`);
+
+ if (links > 0) {
+ return contentFrame;
+ }
+ } catch (error) {
+ console.log(`Error checking frame:`, error.message);
+ continue;
+ }
+ }
+
+ // Fallback: return the first frame's content
+ console.log('Using first frame as fallback');
+ return await screenFrames[0].contentFrame();
+ }
+
+ // Loads the base URL of the app
+ async navigateToApp() {
+ await this.page.goto(this.baseURL);
+ }
+
+ // Selects a form by name and clicks through to start it
+ async selectForm(formName) {
+ const navFrame = await this.getNavigationFrame();
+
+ // Wait for the form link to be available
+ const formLink = navFrame.getByRole('link', { name: formName });
+ await formLink.waitFor({ state: 'visible', timeout: 10000 });
+ await formLink.click();
+
+ // Wait for the follow link button to appear
+ const followButton = navFrame.getByRole('button', { name: CONSTANTS.BUTTONS.FOLLOW_LINK });
+ await followButton.waitFor({ state: 'visible', timeout: 10000 });
+ await followButton.click();
+ }
+
+ // Clicks "Create new instance" to begin filling a form
+ async createNewInstance() {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.getByRole('button', { name: CONSTANTS.BUTTONS.CREATE_NEW_INSTANCE }).click();
+ }
+
+ // Clicks the "Next" button (first found)
+ async clickNext() {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.locator(CONSTANTS.SELECTORS.NEXT_BUTTON).first().click();
+ }
+
+ // Clicks the "Next" button using exact text match
+ async clickNextByText() {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.getByText(CONSTANTS.BUTTONS.NEXT, { exact: true }).click();
+ }
+
+ // Clicks the "Finalize" button to submit the form
+ async finalizeForm() {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.getByRole('button', { name: CONSTANTS.BUTTONS.FINALIZE }).click();
+ }
+
+ // Fills a numeric input field (e.g., rating or quantity)
+ async fillNumericInput(fieldName, value) {
+ const screenFrame = await this.getScreenFrame();
+ const field = screenFrame.getByRole('spinbutton', { name: fieldName });
+ await field.waitFor({ state: 'visible', timeout: 10000 });
+ await field.click();
+ await field.fill(value.toString());
+ }
+
+ // Fills a text input field
+ async fillTextInput(fieldName, value) {
+ const screenFrame = await this.getScreenFrame();
+ const field = screenFrame.getByRole('textbox', { name: fieldName });
+ await field.waitFor({ state: 'visible', timeout: 10000 });
+ await field.click();
+ await field.fill(value);
+ }
+
+ // Tries both spinbutton and textbox roles for numeric input (fallback for form variations)
+ async fillNumericInputAlternative(fieldName, value) {
+ const screenFrame = await this.getScreenFrame();
+ try {
+ const spinField = screenFrame.getByRole('spinbutton', { name: fieldName });
+ await spinField.waitFor({ state: 'visible', timeout: 5000 });
+ await spinField.click();
+ await spinField.fill(value.toString());
+ } catch (error) {
+ // Fallback in case spinbutton doesn't work
+ const textField = screenFrame.getByRole('textbox', { name: fieldName });
+ await textField.waitFor({ state: 'visible', timeout: 5000 });
+ await textField.click();
+ await textField.fill(value.toString());
+ }
+ }
+
+ // Checks a checkbox input
+ async checkCheckbox(fieldName) {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.getByRole('checkbox', { name: fieldName }).check();
+ }
+
+ // Selects a radio button option
+ async selectRadioButton(optionName) {
+ const screenFrame = await this.getScreenFrame();
+ await screenFrame.getByRole('radio', { name: optionName }).check();
+ }
+
+ // Clicks the follow link button (for Agriculture form completion)
+ async clickFollowLinkButton() {
+ const navFrame = await this.getNavigationFrame();
+ await navFrame.getByRole('button', { name: CONSTANTS.BUTTONS.FOLLOW_LINK }).click();
+ }
+}
+
+test.describe('ODK Form Application', () => {
+ let odkPage;
+
+ // Before each test, create the page object and navigate to the app
+ test.beforeEach(async ({ page }) => {
+ odkPage = new ODKFormPage(page);
+ await odkPage.navigateToApp();
+ });
+
+ // Test case: Filling out the "Example Form"
+ test('should complete Example Form with rating and coffee consumption', async () => {
+ const testData = TestDataFactory.getExampleFormData();
+
+ // Load and start the form
+ await odkPage.selectForm(CONSTANTS.FORMS.EXAMPLE);
+ await odkPage.createNewInstance();
+ await odkPage.clickNext();
+
+ // Fill numeric rating field
+ await odkPage.fillNumericInputAlternative(CONSTANTS.EXAMPLE_FORM_FIELDS.INITIAL_RATING, testData.initialRating);
+ await odkPage.clickNext();
+
+ // Check a checkbox (e.g., auto-generated computation field)
+ await odkPage.checkCheckbox(CONSTANTS.EXAMPLE_FORM_FIELDS.COMPUTED_ASSIGNMENT);
+ await odkPage.clickNext();
+
+ // Fill how many cups of coffee
+ await odkPage.fillNumericInputAlternative(CONSTANTS.EXAMPLE_FORM_FIELDS.COFFEE_CUPS, testData.coffeeCups);
+ await odkPage.clickNext();
+
+ // Complete remaining steps
+ await odkPage.clickNext();
+ await odkPage.clickNext();
+
+ // Finalize and verify
+ await odkPage.finalizeForm();
+ await odkPage.clickNext();
+
+ // Basic assertion to confirm app URL
+ await expect(odkPage.page).toHaveURL(CONSTANTS.URL_PATTERNS.LOCALHOST);
+ });
+
+ // Test case: Completing the "Agriculture Form"
+ test('should complete Agriculture Form with corn plant data', async () => {
+ const testData = TestDataFactory.getAgricultureFormData();
+
+ // Load and start the form
+ await odkPage.selectForm(CONSTANTS.FORMS.AGRICULTURE);
+ await odkPage.createNewInstance();
+ await odkPage.clickNextByText();
+
+ // Fill corn variety
+ await odkPage.fillTextInput(CONSTANTS.AGRICULTURE_FORM_FIELDS.CORN_VARIETY, testData.cornVariety);
+ await odkPage.clickNextByText();
+
+ // Select soil condition from radio buttons
+ await odkPage.selectRadioButton(testData.soilCondition);
+ await odkPage.clickNextByText();
+
+ // Fill plant height
+ await odkPage.fillTextInput(CONSTANTS.AGRICULTURE_FORM_FIELDS.PLANT_HEIGHT, testData.plantHeight);
+ await odkPage.clickNextByText();
+
+ // Finalize and end the form
+ await odkPage.finalizeForm();
+
+ // Follow the completion link after finalization
+ await odkPage.clickFollowLinkButton();
+ });
+});
+
+// Run tests in parallel mode
+test.describe.configure({ mode: 'parallel' });
+
+async function verifyFormCompletion(page, expectedElements = []) {
+ for (const element of expectedElements) {
+ await expect(page.locator(element)).toBeVisible();
+ }
+}
+
+class TestDataFactory {
+ static getExampleFormData() {
+ return {
+ initialRating: Math.floor(Math.random() * CONSTANTS.RANGES.RATING.MAX) + CONSTANTS.RANGES.RATING.MIN,
+ coffeeCups: Math.floor(Math.random() * CONSTANTS.RANGES.COFFEE_CUPS.MAX) + CONSTANTS.RANGES.COFFEE_CUPS.MIN
+ };
+ }
+
+ static getAgricultureFormData() {
+ const varieties = ['Corn Variety A', 'Corn Variety B', 'Corn Variety C'];
+ const conditions = Object.values(CONSTANTS.SOIL_CONDITIONS);
+
+ return {
+ cornVariety: varieties[Math.floor(Math.random() * varieties.length)],
+ plantHeight: (Math.floor(Math.random() * (CONSTANTS.RANGES.PLANT_HEIGHT.MAX - CONSTANTS.RANGES.PLANT_HEIGHT.MIN)) + CONSTANTS.RANGES.PLANT_HEIGHT.MIN).toString(),
+ soilCondition: conditions[Math.floor(Math.random() * conditions.length)]
+ };
+ }
+}
\ No newline at end of file
diff --git a/tests/xlsxconvt.spec.js b/tests/xlsxconvt.spec.js
new file mode 100644
index 000000000..8d67596a5
--- /dev/null
+++ b/tests/xlsxconvt.spec.js
@@ -0,0 +1,73 @@
+const { test, expect } = require('@playwright/test');
+const path = require('path');
+const fs = require('fs');
+
+test('XLSX Converter should upload XLSX and download JSON formDef', async ({ page }) => {
+ await page.goto('http://localhost:8000/index.html', { waitUntil: 'networkidle' });
+
+ const converterButton = page.getByRole('button', { name: /XLSX Converter/i });
+ await converterButton.waitFor({ state: 'visible', timeout: 60000 });
+ await converterButton.click();
+
+ await page.waitForSelector('#tab3', { timeout: 30000 });
+ const outerFrame = page.frameLocator('#tab3');
+ await outerFrame.locator('#xlsxscreen').waitFor({ state: 'attached', timeout: 30000 });
+ const innerFrame = outerFrame.frameLocator('#xlsxscreen');
+
+ await innerFrame.locator('input[type="file"]').waitFor({ state: 'visible', timeout: 10000 });
+
+ const filePath = path.join(__dirname, 'assets', 'census.xlsx');
+ const fileInputs = await innerFrame.locator('input[type="file"]').count({ timeout: 10000 });
+
+ if (fileInputs > 0) {
+ const fileInput = innerFrame.locator('input[type="file"]').first();
+ await fileInput.waitFor({ state: 'visible', timeout: 10000 });
+ await fileInput.setInputFiles(filePath);
+ } else {
+ const chooseFileButton = innerFrame.getByRole('button', { name: 'Choose File' });
+ await chooseFileButton.waitFor({ state: 'visible', timeout: 10000 });
+ await chooseFileButton.setInputFiles(filePath);
+ }
+
+ page.once('dialog', async dialog => {
+ try {
+ await dialog.dismiss();
+ console.log('Dialog dismissed:', dialog.message());
+ } catch (err) {
+ console.error('Failed to dismiss dialog:', err);
+ }
+ });
+
+ const saveToFileSystemButton = innerFrame.getByRole('button', { name: /Save to File System/i });
+ await saveToFileSystemButton.waitFor({ state: 'visible', timeout: 15000 });
+ await saveToFileSystemButton.click();
+
+ const downloadPromise = page.waitForEvent('download', { timeout: 30000 });
+ const saveButton = innerFrame.getByRole('button', { name: /^Save$/i });
+ await saveButton.waitFor({ state: 'visible', timeout: 10000 });
+ await saveButton.click();
+
+ const download = await downloadPromise;
+ const outputsDir = path.join(__dirname, 'outputs');
+ if (!fs.existsSync(outputsDir)) {
+ fs.mkdirSync(outputsDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const savePath = path.join(outputsDir, `census_converted_${timestamp}`);
+ await download.saveAs(savePath);
+
+ console.log('Downloaded file:', download.suggestedFilename());
+ console.log('File saved to:', savePath);
+
+ const filename = download.suggestedFilename();
+ expect(filename).toMatch(/(formDef\.json|tableSpecificDefinitions\.js)$/);
+ expect(filename).toBeTruthy();
+ expect(filename.length).toBeGreaterThan(0);
+ expect(fs.existsSync(savePath)).toBe(true);
+
+ const stats = fs.statSync(savePath);
+ expect(stats.size).toBeGreaterThan(0);
+
+ console.log(`Test completed successfully. File size: ${stats.size} bytes`);
+});
diff --git a/zipFile.sh b/zipFile.sh
index ebdc11f53..cbd4be180 100755
--- a/zipFile.sh
+++ b/zipFile.sh
@@ -67,7 +67,7 @@ cp -r "$appConfig/assets/framework"/* "$surveyConfig/assets/framework"
# Survey config img files
cp "$appConfig/assets/img/advance.png" "$surveyConfig/assets/img/advance.png"
cp "$appConfig/assets/img/backup.png" "$surveyConfig/assets/img/backup.png"
-cp "$appConfig/assets/img/form_logo.png" "$surveyConfig/assets/img/form_logo.png"
+cp "$appConfig/assets/img/form_logo_new.png" "$surveyConfig/assets/img/form_logo_new.png"
cp "$appConfig/assets/img/play.png" "$surveyConfig/assets/img/play.png"
#Move all the necessary Survey system files over