diff --git a/README.md b/README.md index 61f4ea29c..27895818a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ License for Android and Windows versions: MIT or Apache 2.0 License for iOS version: MIT only -|Android Circle-CI (**full** suite)|iOS Travis-CI (*very* limited suite)| +|Android Circle-CI (**full** suite)|iOS Travis-CI (partial suite)| |-----------------------|----------------------| |[![Circle CI](https://circleci.com/gh/litehelpers/Cordova-sqlite-storage.svg?style=svg)](https://circleci.com/gh/litehelpers/Cordova-sqlite-storage)|[![Build Status](https://travis-ci.org/litehelpers/Cordova-sqlite-storage.svg)](https://travis-ci.org/litehelpers/Cordova-sqlite-storage)| @@ -124,6 +124,8 @@ See the [Sample section](#sample) for a sample with a more detailed explanation. - Issue with UNICODE `\u0000` character (same as `\0`) - No background processing - INCORRECT error code (0) and INCONSISTENT error message (missing actual error info) in error callbacks ref: [litehelpers/Cordova-sqlite-storage#539](https://github.com/litehelpers/Cordova-sqlite-storage/issues/539) + - Issue with emojis and other 4-octet UTF-8 characters (apparently not stored correctly) + - Not possible to read BLOB column values - It is **not** possible to use this plugin with the default "Any CPU" target. A specific target CPU type **must** be specified when building an app with this plugin. - FTS3, FTS4, and R-Tree support is tested working OK in this version (for all target platforms in this version branch Android/iOS/Windows) - Android is supported back to SDK 10 (a.k.a. Gingerbread, Android 2.3.3); support for older versions is available upon request. @@ -317,7 +319,7 @@ As "strongly recommended" by [Web SQL Database API 8.5 SQL injection](https://ww - This plugin does *not* support the synchronous Web SQL interfaces. - It is possible to request a SQL statement list such as "SELECT 1; SELECT 2" within a single SQL statement string, however the plugin will only execute the first statement and silently ignore the others ref: [litehelpers/Cordova-sqlite-storage#551](https://github.com/litehelpers/Cordova-sqlite-storage/issues/551) - It is possible to insert multiple rows like: `transaction.executeSql('INSERT INTO MyTable VALUES (?,?),(?,?)', ['Alice', 101, 'Betty', 102]);` which was not supported by SQLite 3.6.19 as referenced by [Web SQL API section 5](https://www.w3.org/TR/webdatabase/#web-sql). The iOS WebKit Web SQL implementation seems to support this as well. -- Unlike the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/) this plugin handles executeSql calls with too few parameters without error reporting. +- Unlike the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/) this plugin handles executeSql calls with too few parameters without error reporting. In case of too many parameters this plugin reports error code 0 (SQLError.UNKNOWN_ERR) while Android/iOS (WebKit) Web SQL correctly reports error code 5 (SQLError.SYNTAX_ERR) ref: https://www.w3.org/TR/webdatabase/#dom-sqlexception-code-syntax - Known issue(s) with handling of `Infinity` values (positive or negative) and certain ASCII/UNICODE characters as described below. - Boolean `true` and `false` values are handled by converting them to the "true" and "false" TEXT string values, same as WebKit Web SQL on Android and iOS. This does not seem to be 100% correct as discussed in: [litehelpers/Cordova-sqlite-storage#545](https://github.com/litehelpers/Cordova-sqlite-storage/issues/545) - Certain errors such as CREATE VIRTUAL TABLE USING bogus module are reported with error code 5 (SQLError.SYNTAX_ERR) on Android and iOS. This happens to be the case for WebKit Web SQL (Android/iOS). @@ -325,6 +327,8 @@ As "strongly recommended" by [Web SQL Database API 8.5 SQL injection](https://ww - Issues with error code on Windows as well as Android with the `androidDatabaseImplementation: 2` setting described below. - In case of an issue that causes an API function to throw an exception (Android/iOS WebKit) Web SQL includes includes a code member with value of 0 (SQLError.UNKNOWN_ERR) in the exception while the plugin includes no such code member. - This plugin supports some non-standard features as documented below. +- Results of SELECT with BLOB data such as `SELECT LOWER(X'40414243') AS myresult`, `SELECT X'40414243' AS myresult`, or reading data stored by `INSERT INTO MyTable VALUES (X'40414243')` are not consistent on Android in case the built-in Android database is used (using the `androidDatabaseImplementation: 2` setting in `window.sqlitePlugin.openDatabase`) or Windows. (These work with Android/iOS WebKit Web SQL and have been supported by SQLite for a number of years.) +- The results data objects are not immutable as specified/implied by [Web SQL API section 4.5](https://www.w3.org/TR/webdatabase/#database-query-results). ### Security of deleted data @@ -343,7 +347,8 @@ See **Security of sensitive data** in the [Security](#security) section above. - iOS version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing - As described below, auto-vacuum is NOT enabled by default. -- INSERT statement that affects multiple rows (due to SELECT cause or using TRIGGER(s), for example) does not report proper rowsAffected on Android in case the built-in Android database used (using the `androidDatabaseImplementation` option in `window.sqlitePlugin.openDatabase`) +- The Android and Windows versions do not always handle four-byte UTF-8 characters emoji characters such as `\u1F603` (SMILING FACE, MOUTH OPEN) correctly ref: [litehelpers/Cordova-sqlite-storage#564](https://github.com/litehelpers/Cordova-sqlite-storage/issues/564). It is sometimes possible to store and retrieve such characters but certain operations hex conversions do not work properly with the default [Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector) database implementation. It is suspected that such characters would be stored incorrectly in the Android and Windows versions. This is not an issue in case the built-in Android database is used (using the `androidDatabaseImplementation: 2` setting in `window.sqlitePlugin.openDatabase`) +- INSERT statement that affects multiple rows (due to SELECT cause or using TRIGGER(s), for example) reports incorrect rowsAffected on Android in case the built-in Android database used (using the `androidDatabaseImplementation` option in `window.sqlitePlugin.openDatabase`) - Memory issue observed when adding a large number of records due to the JSON implementation which is improved in [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) (available with GPL or commercial license options) - Infinity (positive or negative) values are not supported (known to be broken on Android, may cause a crash on iOS ref: [litehelpers/Cordova-sqlite-storage#405](https://github.com/litehelpers/Cordova-sqlite-storage/issues/405)) - A stability issue was reported on the iOS version when in use together with [SockJS](http://sockjs.org/) client such as [pusher-js](https://github.com/pusher/pusher-js) at the same time (see [litehelpers/Cordova-sqlite-storage#196](https://github.com/litehelpers/Cordova-sqlite-storage/issues/196)). The workaround is to call sqlite functions and [SockJS](http://sockjs.org/) client functions in separate ticks (using setTimeout with 0 timeout). @@ -352,7 +357,7 @@ See **Security of sensitive data** in the [Security](#security) section above. - Close/delete database bugs described below. - When a database is opened and deleted without closing, the iOS version is known to leak resources. - It is NOT possible to open multiple databases with the same name but in different locations (iOS version). -- Incorrect or missing insertId/rowsAffected in results for INSERT/UPDATE/DELETE SQL statements with extra semicolon(s) in the beginning for Android in case the `androidDatabaseImplementation: 2` (built-in android.database implementation) option is used. +- Incorrect or missing rowsAffected in results for INSERT/UPDATE/DELETE SQL statements with extra semicolon(s) in the beginning for Android in case the `androidDatabaseImplementation: 2` (built-in android.database implementation) option is used. Some more known issues are tracked in the [open Cordova-sqlite-storage bugs](https://github.com/litehelpers/Cordova-sqlite-storage/issues?q=is%3Aissue+is%3Aopen+label%3Abug). @@ -402,7 +407,7 @@ Some more limitations are tracked in the [open Cordova-sqlite-storage documentat - Delete an open database inside a statement or transaction callback. - WITH clause (not supported by some older sqlite3 versions) - _Handling of invalid transaction and transaction.executeSql arguments_ -- _Emojis and other 4-octet UTF-8 characters_ +- More emojis and other 4-octet UTF-8 characters diff --git a/spec/www/spec/db-tx-error-handling-test.js b/spec/www/spec/db-tx-error-handling-test.js index 0c67a8a62..9f53964a1 100644 --- a/spec/www/spec/db-tx-error-handling-test.js +++ b/spec/www/spec/db-tx-error-handling-test.js @@ -388,7 +388,7 @@ var mytests = function() { expect(ex.message).toBeDefined(); if (!isWebSql) - expect(ex.code).toBe(0); // [UNKNOWN_ERR] + expect(ex.code).toBe(0); // (SQLite.UNKNOWN_ERR) if (!isWebSql) expect(ex.message).toMatch(/transaction expected a function/); @@ -441,7 +441,7 @@ var mytests = function() { expect(ex.message).toBeDefined(); if (!isWebSql) - expect(ex.code).toBe(0); // [UNKNOWN_ERR] + expect(ex.code).toBe(0); // (SQLite.UNKNOWN_ERR) if (!isWebSql) expect(ex.message).toMatch(/transaction expected a function/); @@ -580,7 +580,7 @@ var mytests = function() { else if (isWebSql) expect(error.message).toMatch(/the SQLTransactionCallback was null or threw an exception/); else if (isAndroid) - expect(error.message).toMatch(/Cannot call method 'toString' of undefined/); + expect(error.message).toMatch(/Cannot .* 'toString' of undefined/); else expect(error.message).toMatch(/undefined is not an object \(evaluating 'sql\.toString'\)/); @@ -969,7 +969,7 @@ var mytests = function() { else if (isWindows) expect(error.message).toMatch(/Unable to get property 'toString' of undefined or null reference/); else if (isAndroid) - expect(error.message).toMatch(/Cannot call method 'toString' of null/); + expect(error.message).toMatch(/Cannot .* 'toString' of null/); else expect(error.message).toMatch(/null is not an object \(evaluating 'sql\.toString'\)/); @@ -1155,7 +1155,7 @@ var mytests = function() { else if (isWindows) expect(error.message).toMatch(/Unable to get property 'toString' of undefined or null reference/); else if (isAndroid) - expect(error.message).toMatch(/Cannot call method 'toString' of undefined/); + expect(error.message).toMatch(/Cannot .* 'toString' of undefined/); else expect(error.message).toMatch(/undefined is not an object \(evaluating 'sql\.toString'\)/); @@ -1215,7 +1215,7 @@ var mytests = function() { else if (isWindows) expect(ex.message).toMatch(/Unable to get property 'toString' of undefined or null reference/); else if (!isWebSql && isAndroid) - expect(ex.message).toMatch(/Cannot call method 'toString' of undefined/); + expect(ex.message).toMatch(/Cannot .* 'toString' of undefined/); else if (!isWebSql) expect(ex.message).toMatch(/undefined is not an object \(evaluating 'sql\.toString'\)/); else if (isAndroid) @@ -1239,7 +1239,7 @@ var mytests = function() { else if (isWindows) expect(error.message).toMatch(/Unable to get property 'toString' of undefined or null reference/); else if (!isWebSql && isAndroid) - expect(error.message).toMatch(/Cannot call method 'toString' of undefined/); + expect(error.message).toMatch(/Cannot .* 'toString' of undefined/); else if (!isWebSql) expect(error.message).toMatch(/undefined is not an object \(evaluating 'sql\.toString'\)/); else diff --git a/spec/www/spec/db-tx-sql-results.js b/spec/www/spec/db-tx-sql-results.js index 15e6652d1..d46291d35 100644 --- a/spec/www/spec/db-tx-sql-results.js +++ b/spec/www/spec/db-tx-sql-results.js @@ -237,13 +237,30 @@ var mytests = function() { tx.executeSql('DROP TABLE IF EXISTS test_table'); tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (id integer primary key, data text, data_num integer)'); - tx.executeSql('INSERT INTO test_table (data, data_num) VALUES (?,?)', ['test', 100], function(tx, res) { - // check tx & res object parameters: + tx.executeSql('INSERT INTO test_table (data, data_num) VALUES (?,?)', ['test', 100], function(tx, rs) { + // check tx & rs object parameters: expect(tx).toBeDefined(); - expect(res).toBeDefined(); - - expect(res.insertId).toBeDefined(); - expect(res.rowsAffected).toBe(1); + expect(rs).toBeDefined(); + + // From: https://www.sqlite.org/autoinc.html + // > In SQLite, a column with type INTEGER PRIMARY KEY is an alias for the ROWID + // and + // > If the table is initially empty, then a ROWID of 1 is used. + expect(rs.insertId).toBe(1); + expect(rs.rowsAffected).toBe(1); + + // Plugin DEVIATION: + // rs.insertId & res.rowsAffected should be immutable + // ref: https://www.w3.org/TR/webdatabase/#database-query-results + rs.insertId = 2; + rs.rowsAffected = 3; + if (isWebSql) { + expect(rs.insertId).toBe(1); + expect(rs.rowsAffected).toBe(1); + } else { + expect(rs.insertId).toBe(2); + expect(rs.rowsAffected).toBe(3); + } db.transaction(function(tx) { // second tx object: @@ -256,11 +273,21 @@ var mytests = function() { expect(res.rows.item(0).cnt).toBe(1); }); - tx.executeSql('SELECT data_num FROM test_table;', [], function(tx, res) { + tx.executeSql('SELECT data_num FROM test_table;', [], function(tx, rs) { ++check_count; - expect(res.rows.length).toBe(1); - expect(res.rows.item(0).data_num).toBe(100); + expect(rs.rows.length).toBe(1); + expect(rs.rows.item(0).data_num).toBe(100); + + // Plugin DEVIATION: + // rs.rows.length should be immutable + // ref: https://www.w3.org/TR/webdatabase/#database-query-results + rs.rows.length = 2; + if (isWebSql) { + expect(rs.rows.length).toBe(1); + } else { + expect(rs.rows.length).toBe(2); + } }); tx.executeSql('SELECT data FROM test_table;', [], function(tx, res) { @@ -326,7 +353,7 @@ var mytests = function() { expect(temp1.data).toBe('test'); expect(temp2.data).toBe('test'); - // Object from rows.item is immutable in Web SQL but NOT in this plugin: + // Object from rows.item is immutable in Android/iOS WebKit Web SQL but NOT in this plugin: temp1.data = 'another'; if (isWebSql) { @@ -495,9 +522,9 @@ var mytests = function() { }, MYTIMEOUT); it(suiteName + 'tx sql starting with extra semicolon results test', function(done) { - // XXX [BUG #458] BROKEN for android.database & WP8 + // [BUG #458] BROKEN for androidDatabaseImplementation: 2 (built-in android.database) setting if (isWP8) pending('BROKEN for WP8'); - if (isAndroid && isImpl2) pending('BROKEN for android.database implementation'); + if (isAndroid && isImpl2) pending('BROKEN for androidDatabaseImplementation: 2 (built-in android.database) setting'); var db = openDatabase('tx-sql-starting-with-extra-semicolon-results-test.db', '1.0', 'Test', DEFAULT_SIZE); @@ -598,9 +625,14 @@ var mytests = function() { }, MYTIMEOUT); - if (!isWebSql) // NOT supported by Web SQL: - it(suiteName + 'Multi-row INSERT with parameters - NOT supported by Web SQL', function(done) { - if (isWP8) pending('NOT SUPPORTED for WP8'); + describe(scenarioList[i] + ': NON-standard Multi-row INSERT with parameters (post-sqlite 3.6 feature)]', function() { + + it(suiteName + 'Multi-row INSERT with parameters - DEVIATION: (post-sqlite 3.6 feature)' + + ((!isWebSql && isAndroid && isImpl2) ? + ' [SQLResultSet.rowsAffected BROKEN for androidDatabaseImplementation: 2 (built-in android.database)]' : + ''), function(done) { + if (isWP8) pending('SKIP: NOT SUPPORTED for WP8'); + if (isWebSql && isAndroid) pending('SKIP for Android Web SQL'); // FUTURE TBD (??) var db = openDatabase('Multi-row-INSERT-with-parameters-test.db', '1.0', 'Test', DEFAULT_SIZE); @@ -608,9 +640,17 @@ var mytests = function() { tx.executeSql('DROP TABLE IF EXISTS TestTable;'); tx.executeSql('CREATE TABLE TestTable (x,y);'); - tx.executeSql('INSERT INTO TestTable VALUES (?,?),(?,?)', ['a',1,'b',2], function(ignored1, ignored2) { + tx.executeSql('INSERT INTO TestTable VALUES (?,?),(?,?)', ['a',1,'b',2], function(ignored1, rs1) { + expect(rs1).toBeDefined(); + expect(rs1.insertId).toBeDefined(); + expect(rs1.insertId).toBe(2); + if (isWebSql || !(isAndroid && isImpl2)) // [rowsAffected BROKEN for built-in (AOSP) android.database] + expect(rs1.rowsAffected).toBe(2); + else + expect(rs1.rowsAffected).toBe(1); // [ACTUAL (BROKEN) for built-in (AOSP) android.database] + tx.executeSql('SELECT * FROM TestTable', [], function(ignored, resultSet) { - // EXPECTED: CORRECT RESULT: + // EXPECTED (CORRECT RESULT): expect(resultSet.rows.length).toBe(2); expect(resultSet.rows.item(0).x).toBe('a'); expect(resultSet.rows.item(0).y).toBe(1); @@ -621,43 +661,56 @@ var mytests = function() { (isWebSql) ? done() : db.close(done, done); }); }); - }, function(e) { - // ERROR RESULT (NOT EXPECTED): + }, function(error) { + // NOT EXPECTED (ERROR RESULT): expect(false).toBe(true); - expect(e).toBeDefined(); + expect(error.message).toBe('--'); // Close (plugin only) & finish: (isWebSql) ? done() : db.close(done, done); }); }, MYTIMEOUT); + }); + + describe(suiteName + 'NON-STANDARD SQL statement list test(s)', function() { - if (!isWebSql) // NOT covered by Web SQL standard: - it(suiteName + 'INSERT statement list (NOT covered by Web SQL standard) - Plugin BROKEN', function(done) { + it(suiteName + 'INSERT statement list (NOT covered by Web SQL standard) - ' + + (isWebSql ? 'Web SQL ERROR' : 'DEVIATION - PLUGIN BROKEN (with potential data loss)'), function(done) { var db = openDatabase('INSERT-statement-list-test.db', '1.0', 'Test', DEFAULT_SIZE); db.transaction(function(tx) { tx.executeSql('DROP TABLE IF EXISTS TestList;'); tx.executeSql('CREATE TABLE TestList (data);'); - // NOT supported by Web SQL, plugin BROKEN: + // REJECTED by [WebKit] Web SQL; PLUGIN BROKEN with potential data loss + // FUTURE TODO: REJECT BY PLUGIN tx.executeSql('INSERT INTO TestList VALUES (1); INSERT INTO TestList VALUES(2);'); - }, function(e) { - // ERROR RESULT (expected for Web SQL): + }, function(error) { + // ERROR RESULT (expected for Web SQL) if (!isWebSql) expect('Plugin behavior changed').toBe('--'); - expect(e).toBeDefined(); + expect(error).toBeDefined(); + expect(error.code).toBeDefined(); + expect(error.message).toBeDefined(); + + expect(error.code).toBe(5); // (SQLError.SYNTAX_ERR) + + // WebKit Web SQL error message (apparenly with SQLite error code) + if (isWebSql) + expect(error.message).toMatch(/could not prepare statement.*1 not an error/); // Close (plugin only) & finish: (isWebSql) ? done() : db.close(done, done); }, function() { + // TBD ACTUAL RESULT [PLUGIN BROKEN with possible data loss]: if (isWebSql) expect('Unexpected result for Web SQL').toBe('--'); db.transaction(function(tx2) { tx2.executeSql('SELECT * FROM TestList', [], function(ignored, resultSet) { - // CORRECT RESULT for plugin: + // CORRECT RESULT: //expect(resultSet.rows.length).toBe(2); - // ACTUAL RESULT for plugin: + // ACTUAL RESULT for PLUGIN [BROKEN with possible parameter data loss]: expect(resultSet.rows.length).toBe(1); // FIRST ROW CORRECT: @@ -672,42 +725,70 @@ var mytests = function() { }); }, MYTIMEOUT); - if (!isWebSql) // NOT covered by Web SQL standard: - it(suiteName + 'First result from SELECT statement list - NOT covered by Web SQL standard', function(done) { - var db = openDatabase('First-result-from-SELECT-statement-list-test.db', '1.0', 'Test', DEFAULT_SIZE); + it(suiteName + 'executeSql with SELECT statement list - NOT ALLOWED [PLUGIN BROKEN]', function(done) { + // TO FIX ref: https://www.sqlite.org/c3ref/prepare.html + // When calling sqlite3_prepare_v2 check the OUT pzTail pointer + // to ensure there is no other statement afterwards. + // May take some more work for Android & Windows versions. + + var db = openDatabase('tx-sql-with-select-statement-list.db'); db.transaction(function(tx) { - // NOT supported by Web SQL: - tx.executeSql('SELECT UPPER (?) AS upper1; SELECT 1', ['Test string'], function(ignored, resultSet) { + tx.executeSql('SELECT 1; SELECT 2', [], function(ignored, rs) { + // INCORRECT (PLUGIN BROKEN) if (isWebSql) - expect('Unexpected result for Web SQL').toBe('--'); - - expect(resultSet.rows.length).toBe(1); // ACTUAL RESULT for plugin - expect(resultSet.rows.item(0).upper1).toBe('TEST STRING'); + expect('WebKit Web SQL implementation changed (DEVIATION)').toBe('--'); + else + expect(rs).toBeDefined(); - // Close (plugin only) & finish: - (isWebSql) ? done() : db.close(done, done); - }, function(ignored, e) { - // ERROR RESULT (expected for Web SQL): + // EXTRA for INVESTIGATION: statement list with syntax error after the first statement + tx.executeSql('SELECT 1; SLCT 2', [], function(ignored1, rs2) { + expect(rs2).toBeDefined(); + isWebSql ? done() : db.close(done, done); + }, function(ignored, error) { + expect('Plugin behavior changed, please update this test').toBe('--'); + expect(error).toBeDefined(); + // TBD ... + isWebSql ? done() : db.close(done, done); + }); + }, function(ignored, error) { if (!isWebSql) - expect('Plugin behavior changed').toBe('--'); + expect('PLUGIN FIXED, please update this test').toBe('--'); - expect(e).toBeDefined(); + expect(error).toBeDefined(); + expect(error.code).toBeDefined(); + expect(error.message).toBeDefined(); - // Close (plugin only) & finish: - (isWebSql) ? done() : db.close(done, done); - }); + expect(error.code).toBe(5); // (SQLError.SYNTAX_ERR) + + // WebKit Web SQL error message (apparenly with SQLite error code) + if (isWebSql) + expect(error.message).toMatch(/could not prepare statement.*1 not an error/); + + // Close (plugin only), return false, and finish: + return isWebSql ? (done() || false) : + (db.close(done, done) || false); + }) }); }, MYTIMEOUT); - it(suiteName + 'BLOB inserted as a literal', function(done) { - var db = openDatabase('Literal-BLOB-INSERT-test.db', '1.0', 'Test', DEFAULT_SIZE); + }); + + describe(suiteName + 'Binary BLOB data INSERT test(s)', function() { + + it(suiteName + "INSERT Binary literal BLOB data (X'010203'), check results, and check stored data HEX value", function(done) { + var db = openDatabase('Binary-literal-BLOB-data-INSERT-test.db'); db.transaction(function(tx) { tx.executeSql('DROP TABLE IF EXISTS TestTable;'); tx.executeSql('CREATE TABLE TestTable (x);'); - tx.executeSql("INSERT INTO TestTable VALUES (X'010203')", [], function(ignored1, ignored2) { + tx.executeSql("INSERT INTO TestTable VALUES (X'010203')", [], function(ignored, rs1) { + // EXPECTED: CORRECT RESULT: + expect(rs1).toBeDefined(); + expect(rs1.insertId).toBe(1); + expect(rs1.rowsAffected).toBe(1); + tx.executeSql('SELECT HEX(x) AS hex_value FROM TestTable', [], function(ignored, resultSet) { // EXPECTED: CORRECT RESULT: expect(resultSet).toBeDefined(); @@ -729,9 +810,11 @@ var mytests = function() { }); }, MYTIMEOUT); - it(suiteName + 'INSERT with SELECT', function(done) { - if (isAndroid && isImpl2) pending('BUG with android.database implementation'); + }); + describe(suiteName + 'STANDARD multi-row INSERT tests', function() { + + it(suiteName + 'INSERT multiple rows from with SELECT; check results & check stored data [rowsAffected INCORRECT with androidDatabaseImplementation: 2 (built-in android.database) setting]', function(done) { var db = openDatabase('INSERT-with-SELECT-test.db', '1.0', 'Test', DEFAULT_SIZE); db.transaction(function(tx) { @@ -746,12 +829,19 @@ var mytests = function() { // THANKS for GUIDANCE: http://www.tutorialspoint.com/sqlite/sqlite_insert_query.htm tx.executeSql('INSERT INTO tt2 SELECT data FROM tt1;', [], function(ignored1, rs1) { - // EXPECTED: CORRECT RESULT: + // EXPECTED (CORRECT RESULT): expect(rs1).toBeDefined(); - expect(rs1.insertId).toBeDefined(); - expect(rs1.rowsAffected).toBeDefined(); + + // ref: https://www.w3.org/TR/webdatabase/#dom-sqlresultset-insertid + // > If the statement inserted multiple rows, the ID of the last row must be the one returned. + // (insertId acts like sqlite3_last_insert_rowid) + expect(rs1.insertId).toBe(2); + + // [INCORRECT rowsAffected with androidDatabaseImplementation: 2 (built-in android.database) setting] if (!(isAndroid && isImpl2)) expect(rs1.rowsAffected).toBe(2); + else + expect(rs1.rowsAffected).toBe(1); tx.executeSql('SELECT * FROM tt2', [], function(ignored, rs2) { // EXPECTED: CORRECT RESULT: @@ -775,8 +865,7 @@ var mytests = function() { }); }, MYTIMEOUT); - it(suiteName + 'INSERT with TRIGGER', function(done) { - if (isAndroid && isImpl2) pending('BUG with android.database implementation'); + it(suiteName + 'INSERT with TRIGGER & check results [rowsAffected INCORRECT with androidDatabaseImplementation: 2 (built-in android.database) setting]', function(done) { if (isWP8) pending('SKIP (NOT SUPPORTED) for WP8'); // NOT SUPPORTED for WP8 var db = openDatabase('INSERT-with-TRIGGER-test.db', '1.0', 'Test', DEFAULT_SIZE); @@ -788,39 +877,59 @@ var mytests = function() { tx.executeSql('CREATE TABLE tt1 (data);'); tx.executeSql('CREATE TABLE tt2 (data);'); + tx.executeSql('INSERT INTO tt2 VALUES (?)', ['extra1']); + tx.executeSql('INSERT INTO tt2 VALUES (?)', ['extra2']); + // THANKS for GUIDANCE: http://www.tutorialspoint.com/sqlite/sqlite_triggers.htm tx.executeSql("CREATE TRIGGER t1 AFTER INSERT ON tt1 BEGIN INSERT INTO tt2 VALUES(datetime('now')); END;"); tx.executeSql('INSERT INTO tt1 VALUES (?)', ['test-value'], function(ignored1, rs1) { - // EXPECTED: CORRECT RESULT: + // EXPECTED (CORRECT RESULT): expect(rs1).toBeDefined(); - expect(rs1.insertId).toBeDefined(); - expect(rs1.rowsAffected).toBeDefined(); + + // Apparently this is the last INSERT rowid on tt1, + // NOT on tt2 + expect(rs1.insertId).toBe(1); + // [INCORRECT rowsAffected with androidDatabaseImplementation: 2 (built-in android.database) setting] if (!(isAndroid && isImpl2)) expect(rs1.rowsAffected).toBe(2); + else + expect(rs1.rowsAffected).toBe(1); - tx.executeSql('SELECT count(*) AS cnt FROM tt2', [], function(ignored, rs2) { + tx.executeSql('SELECT COUNT(*) AS count1 FROM tt1', [], function(ignored, rs2) { // EXPECTED: CORRECT RESULT: expect(rs2).toBeDefined(); expect(rs2.rows).toBeDefined(); expect(rs2.rows.length).toBe(1); - expect(rs2.rows.item(0).cnt).toBeDefined(); - expect(rs2.rows.item(0).cnt).toBe(1); + expect(rs2.rows.item(0).count1).toBe(1); + + tx.executeSql('SELECT COUNT(*) AS count2 FROM tt2', [], function(ignored, rs3) { + // EXPECTED: CORRECT RESULT: + expect(rs3).toBeDefined(); + expect(rs3.rows).toBeDefined(); + expect(rs3.rows.length).toBe(1); + expect(rs3.rows.item(0).count2).toBe(3); + + // Close (plugin only - always the case in this test) & finish: + (isWebSql) ? done() : db.close(done, done); + }); - // Close (plugin only - always the case in this test) & finish: - (isWebSql) ? done() : db.close(done, done); }); }); - }, function(e) { + }, function(error) { // ERROR RESULT (NOT EXPECTED): expect(false).toBe(true); - expect(e).toBeDefined(); + expect(error.message).toBe('--'); // Close (plugin only) & finish: (isWebSql) ? done() : db.close(done, done); }); }, MYTIMEOUT); + }); + + describe(suiteName + 'ALTER TABLE tests', function() { + it(suiteName + 'ALTER TABLE ADD COLUMN test', function(done) { var dbname = 'ALTER-TABLE-ADD-COLUMN-test.db'; var createdb = openDatabase(dbname, '1.0', 'Test', DEFAULT_SIZE); @@ -934,32 +1043,123 @@ var mytests = function() { } }, MYTIMEOUT); - // FUTURE TODO more +/- INFINITY, NAN tests + }); - it(suiteName + "SELECT abs('9e999') (Infinity) result test", function(done) { - if (isWP8) pending('SKIP for WP(8)'); - if (isAndroid && !isWebSql) pending('SKIP for Android plugin'); - if (!isWP8 && !isWindows && !isAndroid && !isWebSql) pending('SKIP for iOS plugin'); + describe(suiteName + 'Infinity value manipulation results', function() { + + it(suiteName + "SELECT ABS(?) with '9e999' (Infinity) parameter argument" + + ((!isWebSql && isAndroid) ? ' [Android PLUGIN BROKEN: missing result row]' : ''), function(done) { + if (isWP8) pending('SKIP for WP8'); // (no callback received) + if (!isWebSql && !isAndroid && !isWindows && !isWP8) pending('SKIP for iOS plugin due to CRASH'); var db = openDatabase('Infinite-results-test.db', '1.0', 'Test', DEFAULT_SIZE); db.transaction(function(tx) { expect(tx).toBeDefined(); - tx.executeSql('SELECT abs(?) AS absResult', ['9e999'], function(tx, res) { - expect(res).toBeDefined(); - expect(res.rows).toBeDefined(); - expect(res.rows.length).toBe(1); - expect(res.rows.item(0).absResult).toBeDefined(); - expect(res.rows.item(0).absResult).toBe(Infinity); + tx.executeSql('SELECT ABS(?) AS myresult', ['9e999'], function(tx, rs) { + expect(rs).toBeDefined(); + expect(rs.rows).toBeDefined(); + + if (!isWebSql && isAndroid) { + expect(rs.rows.length).toBe(0); + } else { + expect(rs.rows.length).toBe(1); + expect(rs.rows.item(0).myresult).toBeDefined(); + expect(rs.rows.item(0).myresult).toBe(Infinity); + } // Close (plugin only) & finish: (isWebSql) ? done() : db.close(done, done); }); + }, function(error) { + // NOT EXPECTED: + expect(false).toBe(true); + expect(error.message).toBe('---'); + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); }); }, MYTIMEOUT); + }); + + describe(suiteName + 'Inline BLOB value SELECT result tests', function() { + + it(suiteName + "SELECT LOWER(X'40414243')", function(done) { + if (isWindows) pending('SKIP: BROKEN for Windows'); + + var db = openDatabase("Inline-BLOB-lower-result-test.db", "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function(tx) { + + tx.executeSql("SELECT LOWER(X'40414243') AS myresult", [], function(ignored, rs) { + expect(rs).toBeDefined(); + expect(rs.rows).toBeDefined(); + expect(rs.rows.length).toBe(1); + expect(rs.rows.item(0).myresult).toBe('@abc'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }, function(error) { + // NOT EXPECTED: + expect(false).toBe(true); + expect(error.message).toBe('---'); + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }, MYTIMEOUT); + + it(suiteName + "SELECT X'40414243'", function(done) { + if (isWP8) pending('SKIP for WP8'); // [BROKEN: CRASH with uncaught exception] + if (!isWebSql && isAndroid && isImpl2) pending('SKIP: BROKEN for androidDatabaseImplementation: 2'); + if (isWindows) pending('SKIP: BROKEN for Windows'); + + var db = openDatabase("Inline-BLOB-SELECT-result-test.db", "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function(tx) { + + tx.executeSql("SELECT X'40414243' AS myresult", [], function(ignored, rs) { + if (!isWebSql && isAndroid && isImpl2) expect('Behavior changed please update this test').toBe('--'); + expect(rs).toBeDefined(); + expect(rs.rows).toBeDefined(); + expect(rs.rows.length).toBe(1); + expect(rs.rows.item(0).myresult).toBe('@ABC'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }, function(ignored, error) { + // NOT EXPECTED: + expect(false).toBe(true); + expect(error.message).toBe('---'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + + }, function(error) { + if (!isWebSql && isAndroid && isImpl2) { + expect(error).toBeDefined(); + expect(error.code).toBeDefined(); + expect(error.message).toBeDefined(); + + // TBD wrong error code + expect(error.code).toBe(0); + expect(error.message).toMatch(/error callback did not return false: unknown error.*code 0.*Unable to convert BLOB to string/); + } else { + // NOT EXPECTED: + expect(false).toBe(true); + expect(error.message).toBe('---'); + } + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }, MYTIMEOUT); + + }); + }); } diff --git a/spec/www/spec/db-tx-string-test.js b/spec/www/spec/db-tx-string-test.js index 052eddb5d..b6c08fe1a 100755 --- a/spec/www/spec/db-tx-string-test.js +++ b/spec/www/spec/db-tx-string-test.js @@ -7,8 +7,6 @@ var DEFAULT_SIZE = 5000000; // max to avoid popup in safari/ios var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) var isAndroid = !isWindows && /Android/.test(navigator.userAgent); -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] // NOTE: In the common storage-master branch there is no difference between the // default implementation and implementation #2. But the test will also apply @@ -49,8 +47,10 @@ var mytests = function() { } } - it(suiteName + 'US-ASCII String manipulation test', function(done) { - var db = openDatabase("ASCII-string-test.db", "1.0", "Demo", DEFAULT_SIZE); + describe(suiteName + 'basic string binding/manipulation tests', function() { + + it(suiteName + 'Inline US-ASCII String manipulation test with empty ([]) parameter list', function(done) { + var db = openDatabase("Inline-US-ASCII-string-test-with-empty-parameter-list.db", "1.0", "Demo", DEFAULT_SIZE); expect(db).toBeDefined(); @@ -66,6 +66,23 @@ var mytests = function() { }); }, MYTIMEOUT); + it(suiteName + 'Inline US-ASCII String manipulation test with null parameter list', function(done) { + var db = openDatabase("Inline-US-ASCII-string-test-with-null-parameter-list.db", "1.0", "Demo", DEFAULT_SIZE); + + expect(db).toBeDefined(); + + db.transaction(function(tx) { + expect(tx).toBeDefined(); + + tx.executeSql("SELECT UPPER('Some US-ASCII text') AS uppertext", null, function(tx, res) { + expect(res.rows.item(0).uppertext).toBe("SOME US-ASCII TEXT"); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }); + }, MYTIMEOUT); + it(suiteName + 'US-ASCII String binding test', function(done) { var db = openDatabase("ASCII-string-binding-test.db", "1.0", "Demo", DEFAULT_SIZE); @@ -95,9 +112,9 @@ var mytests = function() { }, MYTIMEOUT); it(suiteName + 'String encoding test with UNICODE \\u0000', function (done) { - if (isWindows) pending('BROKEN for Windows'); // XXX if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (isAndroid && !isWebSql && !isImpl2) pending('BROKEN for Android (default sqlite-connector version)'); // XXX + if (isWindows) pending('BROKEN for Windows'); // [FUTURE TBD, already documented] + if (!isWebSql && isAndroid && !isImpl2) pending('BROKEN for Android (default sqlite-connector version)'); // [FUTURE TBD (documented)] var dbName = "Unicode-hex-test"; var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); @@ -231,6 +248,92 @@ var mytests = function() { }); }, MYTIMEOUT); + // NOTE On Windows [using a customized version of the SQLite3-WinRT component] and + // default Android-sqlite-connector implementations it is possible to manipulate, + // store, and retrieve a text string with 4-octet UTF-8 characters such as emojis. + // However HEX manipulations do not work the same as Android/iOS WebKit Web SQL, + // iOS plugin, or Android plugin with androidDatabaseImplementation : 2 setting. + // This linkely indicates that such characters are stored differently + // due to UTF-8 string handling limitations of Android-sqlite-connector + // and Windows (SQLite3-WinRT) versions. ref: litehelpers/Cordova-sqlite-storage#564 + + it(suiteName + 'Inline emoji string manipulation test: SELECT UPPER("a\\uD83D\\uDE03.") [\\u1F603 SMILING FACE (MOUTH OPEN)]', function(done) { + var db = openDatabase("Inline-emoji-hex-test.db", "1.0", "Demo", DEFAULT_SIZE); + expect(db).toBeDefined(); + + db.transaction(function(tx) { + expect(tx).toBeDefined(); + + tx.executeSql('SELECT UPPER("a\uD83D\uDE03.") AS uppertext', [], function(tx, res) { + expect(res.rows.item(0).uppertext).toBe('A\uD83D\uDE03.'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }); + }, MYTIMEOUT); + + it(suiteName + 'Inline emoji HEX test: SELECT HEX("@\\uD83D\\uDE03!") [\\u1F603 SMILING FACE (MOUTH OPEN)]', function(done) { + if (isWP8) pending('BROKEN for WP8'); + if (isAndroid && !isWebSql && !isImpl2) pending('BROKEN for Android (default sqlite-connector version)'); + if (isWindows) pending('BROKEN for Windows'); + + var db = openDatabase("Inline-emoji-hex-test.db", "1.0", "Demo", DEFAULT_SIZE); + expect(db).toBeDefined(); + + db.transaction(function(tx) { + expect(tx).toBeDefined(); + + tx.executeSql('SELECT HEX("@\uD83D\uDE03!") AS hexvalue', [], function(tx, res) { + expect(res.rows.item(0).hexvalue).toBe('40F09F988321'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }); + }, MYTIMEOUT); + + it(suiteName + "Inline BLOB with emoji string manipulation test: SELECT LOWER(X'41F09F9883') [A\uD83D\uDE03] [\\u1F603 SMILING FACE (MOUTH OPEN)]", function(done) { + if (isWP8) pending('BROKEN for WP8'); // [CRASH with uncaught exception] + if (isAndroid && !isWebSql && !isImpl2) pending('BROKEN for Android (default sqlite-connector version)'); + if (isWindows) pending('BROKEN for Windows'); + + var db = openDatabase("Inline-emoji-select-lower-result-test.db", "1.0", "Demo", DEFAULT_SIZE); + expect(db).toBeDefined(); + + db.transaction(function(tx) { + expect(tx).toBeDefined(); + + tx.executeSql("SELECT LOWER(X'41F09F9883') AS lowertext", [], function(ignored, res) { + expect(res).toBeDefined(); + expect(res.rows.item(0).lowertext).toBe('a\uD83D\uDE03'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }); + }, MYTIMEOUT); + + it(suiteName + 'emoji SELECT HEX(?) parameter value test: "@\\uD83D\\uDE03!" [\\u1F603 SMILING FACE (MOUTH OPEN)]', function(done) { + if (isWP8) pending('BROKEN for WP8'); + if (isAndroid && !isWebSql && !isImpl2) pending('BROKEN for Android (default sqlite-connector version)'); + if (isWindows) pending('BROKEN for Windows'); + + var db = openDatabase("String-emoji-parameter-value-test.db", "1.0", "Demo", DEFAULT_SIZE); + expect(db).toBeDefined(); + + db.transaction(function(tx) { + expect(tx).toBeDefined(); + + tx.executeSql('SELECT HEX(?) AS hexvalue', ['@\uD83D\uDE03!'], function(tx, res) { + expect(res.rows.item(0).hexvalue).toBe('40F09F988321'); + + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + }); + }, MYTIMEOUT); + // NOTE: the next two tests show that for iOS: // - UNICODE \u2028 line separator from Javascript to Objective-C is working ok // - UNICODE \u2028 line separator from Objective-C to Javascript is BROKEN @@ -257,9 +360,8 @@ var mytests = function() { }, MYTIMEOUT); it(suiteName + ' handles UNICODE \\u2028 line separator correctly [string test]', function (done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (!(isWebSql || isAndroid || isIE)) pending('BROKEN for iOS'); // XXX [BUG #147] (no callback received) + if (!isWebSql && !isAndroid && !isWindows && !isWP8) pending('BROKEN for iOS plugin'); // [BUG #147] (no callback received) // NOTE: since the above test shows the UNICODE line separator (\u2028) // is seen by the sqlite implementation OK, it is now concluded that @@ -286,7 +388,7 @@ var mytests = function() { // - UNICODE \u2029 line separator from Javascript to Objective-C is working ok // - UNICODE \u2029 line separator from Objective-C to Javascript is BROKEN // ref: litehelpers/Cordova-sqlite-storage#147 - it(suiteName + "UNICODE \\u2029 line separator string length", function(done) { + it(suiteName + "UNICODE \\u2029 paragraph separator string length", function(done) { if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) // NOTE: this test verifies that the UNICODE paragraph separator (\u2029) @@ -308,10 +410,9 @@ var mytests = function() { }); }, MYTIMEOUT); - it(suiteName + ' handles UNICODE \\u2029 line separator correctly [string test]', function (done) { - + it(suiteName + ' handles UNICODE \\u2029 paragraph separator correctly [string test]', function (done) { if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (!(isWebSql || isAndroid || isIE)) pending('BROKEN for iOS'); // XXX [BUG #147] (no callback received) + if (!isWebSql && !isAndroid && !isWindows && !isWP8) pending('BROKEN for iOS plugin'); // [BUG #147] (no callback received) // NOTE: since the above test shows the UNICODE paragraph separator (\u2029) // is seen by the sqlite implementation OK, it is now concluded that @@ -423,6 +524,10 @@ var mytests = function() { }); }, MYTIMEOUT); + }); + + describe(suiteName + 'string test with non-primitive parameter values', function() { + it(suiteName + 'String test with new String object', function(done) { var db = openDatabase("String-object-string-test.db", "1.0", "Demo", DEFAULT_SIZE); @@ -437,13 +542,13 @@ var mytests = function() { }); }, MYTIMEOUT); - it(suiteName + 'String test with custom object', function(done) { - // MyCustomObject "class": - function MyCustomObject() {}; - MyCustomObject.prototype.toString = function() {return 'toString result';}; - MyCustomObject.prototype.valueOf = function() {return 'valueOf result';}; + it(suiteName + 'String test with custom object parameter value', function(done) { + // MyCustomParameterObject "class": + function MyCustomParameterObject() {}; + MyCustomParameterObject.prototype.toString = function() {return 'toString result';}; + MyCustomParameterObject.prototype.valueOf = function() {return 'valueOf result';}; - var myObject = new MyCustomObject(); + var myObject = new MyCustomParameterObject(); // Check myObject: expect(myObject.toString()).toBe('toString result'); expect(myObject.valueOf()).toBe('valueOf result'); @@ -460,8 +565,40 @@ var mytests = function() { }); }); }, MYTIMEOUT); + }); + + describe(suiteName + 'string test with [non-primitive] values for SQL', function() { + + it(suiteName + 'String test with new String for SQL', function(done) { + var myNewString = new String("SELECT UPPER('Alice') as u1"); + + var db = openDatabase("New-string-for-sql-test.db", "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function(tx) { + tx.executeSql(myNewString, [], function(tx_ignored, resultSet) { + // EXPECTED RESULT: + expect(true).toBe(true); + expect(resultSet).toBeDefined(); + expect(resultSet.rows).toBeDefined(); + expect(resultSet.rows.length).toBe(1); + expect(resultSet.rows.item(0)).toBeDefined(); + expect(resultSet.rows.item(0).u1).toBeDefined(); + expect(resultSet.rows.item(0).u1).toBe('ALICE'); + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + + }, function(tx_ignored, error) { + // NOT EXPECTED: + expect(false).toBe(true); + expect(error.message).toBe('--'); + // Close (plugin only) & finish: + (isWebSql) ? done() : db.close(done, done); + }); + + }); + }, MYTIMEOUT); - it(suiteName + 'String test with custom object in place of sql', function(done) { + it(suiteName + 'String test with custom object for SQL', function(done) { // MyCustomObject "class": function MyCustomObject() {}; MyCustomObject.prototype.toString = function() {return "SELECT UPPER('Alice') as u1";}; @@ -472,9 +609,7 @@ var mytests = function() { expect(myObject.toString()).toBe("SELECT UPPER('Alice') as u1"); expect(myObject.valueOf()).toBe("SELECT UPPER('Betty') as u1"); - var db = openDatabase("Custom-sql-object-test.db", "1.0", "Demo", DEFAULT_SIZE); - - var check1 = false; + var db = openDatabase("Custom-object-for-sql-test.db", "1.0", "Demo", DEFAULT_SIZE); db.transaction(function(tx) { tx.executeSql(myObject, [], function(tx_ignored, resultSet) { @@ -500,16 +635,20 @@ var mytests = function() { }); }, MYTIMEOUT); + }); + + describe(suiteName + 'BLOB string test(s)', function() { + it(suiteName + "SELECT HEX(X'010203') [BLOB value test]", function(done) { var db = openDatabase("SELECT-HEX-BLOB-test.db", "1.0", "Demo", DEFAULT_SIZE); db.transaction(function(tx) { - tx.executeSql("SELECT HEX(X'010203') AS hex_value", [], function(ignored, rs) { + tx.executeSql("SELECT HEX(X'010203') AS hexvalue", [], function(ignored, rs) { expect(rs).toBeDefined(); expect(rs.rows).toBeDefined(); expect(rs.rows.length).toBe(1); - expect(rs.rows.item(0).hex_value).toBe('010203'); + expect(rs.rows.item(0).hexvalue).toBe('010203'); // Close (plugin only) & finish: (isWebSql) ? done() : db.close(done, done); @@ -517,6 +656,8 @@ var mytests = function() { }); }, MYTIMEOUT); + }); + }); } diff --git a/spec/www/spec/db-tx-value-bindings-test.js b/spec/www/spec/db-tx-value-bindings-test.js index d8dc01535..83ff6cc60 100755 --- a/spec/www/spec/db-tx-value-bindings-test.js +++ b/spec/www/spec/db-tx-value-bindings-test.js @@ -51,15 +51,15 @@ var mytests = function() { for (var i=0; i