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}}} -
-
- -
-
+
+{{!-- logo removed--}} + {{{localizeText display.instances_survey_form_identification}}} + +
+
+
+
+
+ +
+
+
+
+
+ + {{{localizeText display.instances_previously_created_instances_label}}} +
+
+ +
+
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