prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v4.dart' as v4;
|
||||
import 'schema_v5.dart' as v5;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
case 5:
|
||||
return v5.DatabaseAtV5(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3, 4, 5];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v1.dart' as v1;
|
||||
import 'generated/schema_v2.dart' as v2;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
// These simple tests verify all possible schema updates with a simple (no
|
||||
// data) migration. This is a quick way to ensure that written database
|
||||
// migrations properly alter the schema.
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = BangDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The following template shows how to write tests ensuring your migrations
|
||||
// preserve existing data.
|
||||
// Testing this can be useful for migrations that change existing columns
|
||||
// (e.g. by alterating their type or constraints). Migrations that only add
|
||||
// tables or columns typically don't need these advanced tests. For more
|
||||
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
|
||||
// TODO: This generated template shows how these tests could be written. Adopt
|
||||
// it to your own needs when testing migrations with data integrity.
|
||||
test('migration from v1 to v2 does not corrupt data', () async {
|
||||
// Add data to insert into the old database, and the expected rows after the
|
||||
// migration.
|
||||
// TODO: Fill these lists
|
||||
final oldBangData = <v1.BangData>[];
|
||||
final expectedNewBangData = <v2.BangData>[];
|
||||
|
||||
final oldBangSyncData = <v1.BangSyncData>[];
|
||||
final expectedNewBangSyncData = <v2.BangSyncData>[];
|
||||
|
||||
final oldBangFrequencyData = <v1.BangFrequencyData>[];
|
||||
final expectedNewBangFrequencyData = <v2.BangFrequencyData>[];
|
||||
|
||||
final oldBangHistoryData = <v1.BangHistoryData>[];
|
||||
final expectedNewBangHistoryData = <v2.BangHistoryData>[];
|
||||
|
||||
final oldBangFtsData = <v1.BangFtsData>[];
|
||||
final expectedNewBangFtsData = <v2.BangFtsData>[];
|
||||
|
||||
await verifier.testWithDataIntegrity(
|
||||
oldVersion: 1,
|
||||
newVersion: 2,
|
||||
createOld: v1.DatabaseAtV1.new,
|
||||
createNew: v2.DatabaseAtV2.new,
|
||||
openTestedDatabase: BangDatabase.new,
|
||||
createItems: (batch, oldDb) {
|
||||
batch.insertAll(oldDb.bang, oldBangData);
|
||||
batch.insertAll(oldDb.bangSync, oldBangSyncData);
|
||||
batch.insertAll(oldDb.bangFrequency, oldBangFrequencyData);
|
||||
batch.insertAll(oldDb.bangHistory, oldBangHistoryData);
|
||||
batch.insertAll(oldDb.bangFts, oldBangFtsData);
|
||||
},
|
||||
validateItems: (newDb) async {
|
||||
expect(expectedNewBangData, await newDb.select(newDb.bang).get());
|
||||
expect(
|
||||
expectedNewBangSyncData,
|
||||
await newDb.select(newDb.bangSync).get(),
|
||||
);
|
||||
expect(
|
||||
expectedNewBangFrequencyData,
|
||||
await newDb.select(newDb.bangFrequency).get(),
|
||||
);
|
||||
expect(
|
||||
expectedNewBangHistoryData,
|
||||
await newDb.select(newDb.bangHistory).get(),
|
||||
);
|
||||
expect(expectedNewBangFtsData, await newDb.select(newDb.bangFts).get());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v4.dart' as v4;
|
||||
import 'schema_v5.dart' as v5;
|
||||
import 'schema_v6.dart' as v6;
|
||||
import 'schema_v7.dart' as v7;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
case 5:
|
||||
return v5.DatabaseAtV5(db);
|
||||
case 6:
|
||||
return v6.DatabaseAtV6(db);
|
||||
case 7:
|
||||
return v7.DatabaseAtV7(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [2, 3, 4, 5, 6, 7];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v2.dart' as v2;
|
||||
import 'generated/schema_v3.dart' as v3;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
// These simple tests verify all possible schema updates with a simple (no
|
||||
// data) migration. This is a quick way to ensure that written database
|
||||
// migrations properly alter the schema.
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = TabDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The following template shows how to write tests ensuring your migrations
|
||||
// preserve existing data.
|
||||
// Testing this can be useful for migrations that change existing columns
|
||||
// (e.g. by alterating their type or constraints). Migrations that only add
|
||||
// tables or columns typically don't need these advanced tests. For more
|
||||
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
|
||||
// TODO: This generated template shows how these tests could be written. Adopt
|
||||
// it to your own needs when testing migrations with data integrity.
|
||||
test('migration from v2 to v3 does not corrupt data', () async {
|
||||
// Add data to insert into the old database, and the expected rows after the
|
||||
// migration.
|
||||
// TODO: Fill these lists
|
||||
final oldContainerData = <v2.ContainerData>[];
|
||||
final expectedNewContainerData = <v3.ContainerData>[];
|
||||
|
||||
final oldTabData = <v2.TabData>[];
|
||||
final expectedNewTabData = <v3.TabData>[];
|
||||
|
||||
final oldTabFtsData = <v2.TabFtsData>[];
|
||||
final expectedNewTabFtsData = <v3.TabFtsData>[];
|
||||
|
||||
await verifier.testWithDataIntegrity(
|
||||
oldVersion: 2,
|
||||
newVersion: 3,
|
||||
createOld: v2.DatabaseAtV2.new,
|
||||
createNew: v3.DatabaseAtV3.new,
|
||||
openTestedDatabase: TabDatabase.new,
|
||||
createItems: (batch, oldDb) {
|
||||
batch.insertAll(oldDb.container, oldContainerData);
|
||||
batch.insertAll(oldDb.tab, oldTabData);
|
||||
batch.insertAll(oldDb.tabFts, oldTabFtsData);
|
||||
},
|
||||
validateItems: (newDb) async {
|
||||
expect(
|
||||
expectedNewContainerData,
|
||||
await newDb.select(newDb.container).get(),
|
||||
);
|
||||
expect(expectedNewTabData, await newDb.select(newDb.tab).get());
|
||||
expect(expectedNewTabFtsData, await newDb.select(newDb.tabFts).get());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"_meta": {
|
||||
"description": "This file contains a serialized version of schema entities for drift.",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
"options": {
|
||||
"store_date_time_values_as_text": false
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": 0,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "top_site",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"getter_name": "title",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"getter_name": "url",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const UriConverter()",
|
||||
"dart_type_name": "Uri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"getter_name": "source",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<StoredTopSiteSource>(StoredTopSiteSource.values)",
|
||||
"dart_type_name": "StoredTopSiteSource"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "order_key",
|
||||
"getter_name": "orderKey",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"getter_name": "createdAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": [
|
||||
"UNIQUE(url)"
|
||||
],
|
||||
"unique_keys": [
|
||||
[
|
||||
"url"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"references": [
|
||||
0
|
||||
],
|
||||
"type": "index",
|
||||
"data": {
|
||||
"on": 0,
|
||||
"name": "idx_top_site_order_key",
|
||||
"sql": "CREATE INDEX idx_top_site_order_key ON top_site(order_key);",
|
||||
"unique": false,
|
||||
"columns": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "top_site_seed_state",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "seed_id",
|
||||
"getter_name": "seedId",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "applied_at",
|
||||
"getter_name": "appliedAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"fixed_sql": [
|
||||
{
|
||||
"name": "top_site",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"top_site\" (\"id\" TEXT PRIMARY KEY NOT NULL, \"title\" TEXT NOT NULL, \"url\" TEXT NOT NULL, \"source\" INTEGER NOT NULL, \"order_key\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, UNIQUE(url));"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_top_site_order_key",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE INDEX idx_top_site_order_key ON top_site (order_key)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "top_site_seed_state",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"top_site_seed_state\" (\"seed_id\" TEXT PRIMARY KEY NOT NULL, \"applied_at\" INTEGER NOT NULL);"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1];
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class TopSite extends Table with TableInfo {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
TopSite(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> url = GeneratedColumn<String>(
|
||||
'url',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> source = GeneratedColumn<int>(
|
||||
'source',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> orderKey = GeneratedColumn<String>(
|
||||
'order_key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> createdAt = GeneratedColumn<int>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
source,
|
||||
orderKey,
|
||||
createdAt,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'top_site';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{url},
|
||||
];
|
||||
@override
|
||||
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||
}
|
||||
|
||||
@override
|
||||
TopSite createAlias(String alias) {
|
||||
return TopSite(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => const ['UNIQUE(url)'];
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class TopSiteSeedState extends Table with TableInfo {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
TopSiteSeedState(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> seedId = GeneratedColumn<String>(
|
||||
'seed_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> appliedAt = GeneratedColumn<int>(
|
||||
'applied_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [seedId, appliedAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'top_site_seed_state';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {seedId};
|
||||
@override
|
||||
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||
}
|
||||
|
||||
@override
|
||||
TopSiteSeedState createAlias(String alias) {
|
||||
return TopSiteSeedState(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class DatabaseAtV1 extends GeneratedDatabase {
|
||||
DatabaseAtV1(QueryExecutor e) : super(e);
|
||||
late final TopSite topSite = TopSite(this);
|
||||
late final Index idxTopSiteOrderKey = Index(
|
||||
'idx_top_site_order_key',
|
||||
'CREATE INDEX idx_top_site_order_key ON top_site (order_key)',
|
||||
);
|
||||
late final TopSiteSeedState topSiteSeedState = TopSiteSeedState(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
topSite,
|
||||
idxTopSiteOrderKey,
|
||||
topSiteSeedState,
|
||||
];
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v1.dart' as v1;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = TopSiteDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('v1 schema creation works', () async {
|
||||
final schema = await verifier.schemaAt(1);
|
||||
final db = TopSiteDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 1);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
{
|
||||
"_meta": {
|
||||
"description": "This file contains a serialized version of schema entities for drift.",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
"options": {
|
||||
"store_date_time_values_as_text": false
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": 0,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "setting",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "key",
|
||||
"getter_name": "key",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "partition_key",
|
||||
"getter_name": "partitionKey",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": "",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"getter_name": "value",
|
||||
"moor_type": "any",
|
||||
"nullable": true,
|
||||
"customConstraints": "",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": [],
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "icon_cache",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "origin",
|
||||
"getter_name": "origin",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "icon_data",
|
||||
"getter_name": "iconData",
|
||||
"moor_type": "blob",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "fetch_date",
|
||||
"getter_name": "fetchDate",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "onboarding",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "revision",
|
||||
"getter_name": "revision",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "completion_date",
|
||||
"getter_name": "completionDate",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "riverpod",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "key",
|
||||
"getter_name": "key",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "json",
|
||||
"getter_name": "json",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "expireAt",
|
||||
"getter_name": "expireAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": "",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "destroyKey",
|
||||
"getter_name": "destroyKey",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": "",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": true,
|
||||
"constraints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "toolbar_button_configs",
|
||||
"was_declared_in_moor": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "button_id",
|
||||
"getter_name": "buttonId",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL PRIMARY KEY",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
"primary-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "order_key",
|
||||
"getter_name": "orderKey",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "is_visible",
|
||||
"getter_name": "isVisible",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": "NOT NULL DEFAULT 1",
|
||||
"default_dart": "const CustomExpression('1')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "fallback_id",
|
||||
"getter_name": "fallbackId",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": "",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": [],
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"fixed_sql": [
|
||||
{
|
||||
"name": "setting",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "icon_cache",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onboarding",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "riverpod",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toolbar_button_configs",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1, \"fallback_id\" TEXT) STRICT;"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3];
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
// dart format width=80
|
||||
import 'dart:typed_data' as i2;
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Setting extends Table with TableInfo<Setting, SettingData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Setting(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> key = GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> partitionKey = GeneratedColumn<String>(
|
||||
'partition_key',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
late final GeneratedColumn<DriftAny> value = GeneratedColumn<DriftAny>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.any,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [key, partitionKey, value];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'setting';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
SettingData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return SettingData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
partitionKey: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}partition_key'],
|
||||
),
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.any,
|
||||
data['${effectivePrefix}value'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Setting createAlias(String alias) {
|
||||
return Setting(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class SettingData extends DataClass implements Insertable<SettingData> {
|
||||
final String key;
|
||||
final String? partitionKey;
|
||||
final DriftAny? value;
|
||||
const SettingData({required this.key, this.partitionKey, this.value});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['key'] = Variable<String>(key);
|
||||
if (!nullToAbsent || partitionKey != null) {
|
||||
map['partition_key'] = Variable<String>(partitionKey);
|
||||
}
|
||||
if (!nullToAbsent || value != null) {
|
||||
map['value'] = Variable<DriftAny>(value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory SettingData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return SettingData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
partitionKey: serializer.fromJson<String?>(json['partitionKey']),
|
||||
value: serializer.fromJson<DriftAny?>(json['value']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'partitionKey': serializer.toJson<String?>(partitionKey),
|
||||
'value': serializer.toJson<DriftAny?>(value),
|
||||
};
|
||||
}
|
||||
|
||||
SettingData copyWith({
|
||||
String? key,
|
||||
Value<String?> partitionKey = const Value.absent(),
|
||||
Value<DriftAny?> value = const Value.absent(),
|
||||
}) => SettingData(
|
||||
key: key ?? this.key,
|
||||
partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey,
|
||||
value: value.present ? value.value : this.value,
|
||||
);
|
||||
SettingData copyWithCompanion(SettingCompanion data) {
|
||||
return SettingData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
partitionKey: data.partitionKey.present
|
||||
? data.partitionKey.value
|
||||
: this.partitionKey,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SettingData(')
|
||||
..write('key: $key, ')
|
||||
..write('partitionKey: $partitionKey, ')
|
||||
..write('value: $value')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, partitionKey, value);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is SettingData &&
|
||||
other.key == this.key &&
|
||||
other.partitionKey == this.partitionKey &&
|
||||
other.value == this.value);
|
||||
}
|
||||
|
||||
class SettingCompanion extends UpdateCompanion<SettingData> {
|
||||
final Value<String> key;
|
||||
final Value<String?> partitionKey;
|
||||
final Value<DriftAny?> value;
|
||||
final Value<int> rowid;
|
||||
const SettingCompanion({
|
||||
this.key = const Value.absent(),
|
||||
this.partitionKey = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
SettingCompanion.insert({
|
||||
required String key,
|
||||
this.partitionKey = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : key = Value(key);
|
||||
static Insertable<SettingData> custom({
|
||||
Expression<String>? key,
|
||||
Expression<String>? partitionKey,
|
||||
Expression<DriftAny>? value,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (key != null) 'key': key,
|
||||
if (partitionKey != null) 'partition_key': partitionKey,
|
||||
if (value != null) 'value': value,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
SettingCompanion copyWith({
|
||||
Value<String>? key,
|
||||
Value<String?>? partitionKey,
|
||||
Value<DriftAny?>? value,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return SettingCompanion(
|
||||
key: key ?? this.key,
|
||||
partitionKey: partitionKey ?? this.partitionKey,
|
||||
value: value ?? this.value,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (key.present) {
|
||||
map['key'] = Variable<String>(key.value);
|
||||
}
|
||||
if (partitionKey.present) {
|
||||
map['partition_key'] = Variable<String>(partitionKey.value);
|
||||
}
|
||||
if (value.present) {
|
||||
map['value'] = Variable<DriftAny>(value.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SettingCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('partitionKey: $partitionKey, ')
|
||||
..write('value: $value, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class IconCache extends Table with TableInfo<IconCache, IconCacheData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
IconCache(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> origin = GeneratedColumn<String>(
|
||||
'origin',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<i2.Uint8List> iconData =
|
||||
GeneratedColumn<i2.Uint8List>(
|
||||
'icon_data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<DateTime> fetchDate = GeneratedColumn<DateTime>(
|
||||
'fetch_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [origin, iconData, fetchDate];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'icon_cache';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {origin};
|
||||
@override
|
||||
IconCacheData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return IconCacheData(
|
||||
origin: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}origin'],
|
||||
)!,
|
||||
iconData: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.blob,
|
||||
data['${effectivePrefix}icon_data'],
|
||||
)!,
|
||||
fetchDate: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}fetch_date'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconCache createAlias(String alias) {
|
||||
return IconCache(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class IconCacheData extends DataClass implements Insertable<IconCacheData> {
|
||||
final String origin;
|
||||
final i2.Uint8List iconData;
|
||||
final DateTime fetchDate;
|
||||
const IconCacheData({
|
||||
required this.origin,
|
||||
required this.iconData,
|
||||
required this.fetchDate,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['origin'] = Variable<String>(origin);
|
||||
map['icon_data'] = Variable<i2.Uint8List>(iconData);
|
||||
map['fetch_date'] = Variable<DateTime>(fetchDate);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory IconCacheData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return IconCacheData(
|
||||
origin: serializer.fromJson<String>(json['origin']),
|
||||
iconData: serializer.fromJson<i2.Uint8List>(json['iconData']),
|
||||
fetchDate: serializer.fromJson<DateTime>(json['fetchDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'origin': serializer.toJson<String>(origin),
|
||||
'iconData': serializer.toJson<i2.Uint8List>(iconData),
|
||||
'fetchDate': serializer.toJson<DateTime>(fetchDate),
|
||||
};
|
||||
}
|
||||
|
||||
IconCacheData copyWith({
|
||||
String? origin,
|
||||
i2.Uint8List? iconData,
|
||||
DateTime? fetchDate,
|
||||
}) => IconCacheData(
|
||||
origin: origin ?? this.origin,
|
||||
iconData: iconData ?? this.iconData,
|
||||
fetchDate: fetchDate ?? this.fetchDate,
|
||||
);
|
||||
IconCacheData copyWithCompanion(IconCacheCompanion data) {
|
||||
return IconCacheData(
|
||||
origin: data.origin.present ? data.origin.value : this.origin,
|
||||
iconData: data.iconData.present ? data.iconData.value : this.iconData,
|
||||
fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('IconCacheData(')
|
||||
..write('origin: $origin, ')
|
||||
..write('iconData: $iconData, ')
|
||||
..write('fetchDate: $fetchDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is IconCacheData &&
|
||||
other.origin == this.origin &&
|
||||
$driftBlobEquality.equals(other.iconData, this.iconData) &&
|
||||
other.fetchDate == this.fetchDate);
|
||||
}
|
||||
|
||||
class IconCacheCompanion extends UpdateCompanion<IconCacheData> {
|
||||
final Value<String> origin;
|
||||
final Value<i2.Uint8List> iconData;
|
||||
final Value<DateTime> fetchDate;
|
||||
final Value<int> rowid;
|
||||
const IconCacheCompanion({
|
||||
this.origin = const Value.absent(),
|
||||
this.iconData = const Value.absent(),
|
||||
this.fetchDate = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
IconCacheCompanion.insert({
|
||||
required String origin,
|
||||
required i2.Uint8List iconData,
|
||||
required DateTime fetchDate,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : origin = Value(origin),
|
||||
iconData = Value(iconData),
|
||||
fetchDate = Value(fetchDate);
|
||||
static Insertable<IconCacheData> custom({
|
||||
Expression<String>? origin,
|
||||
Expression<i2.Uint8List>? iconData,
|
||||
Expression<DateTime>? fetchDate,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (origin != null) 'origin': origin,
|
||||
if (iconData != null) 'icon_data': iconData,
|
||||
if (fetchDate != null) 'fetch_date': fetchDate,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
IconCacheCompanion copyWith({
|
||||
Value<String>? origin,
|
||||
Value<i2.Uint8List>? iconData,
|
||||
Value<DateTime>? fetchDate,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return IconCacheCompanion(
|
||||
origin: origin ?? this.origin,
|
||||
iconData: iconData ?? this.iconData,
|
||||
fetchDate: fetchDate ?? this.fetchDate,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (origin.present) {
|
||||
map['origin'] = Variable<String>(origin.value);
|
||||
}
|
||||
if (iconData.present) {
|
||||
map['icon_data'] = Variable<i2.Uint8List>(iconData.value);
|
||||
}
|
||||
if (fetchDate.present) {
|
||||
map['fetch_date'] = Variable<DateTime>(fetchDate.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('IconCacheCompanion(')
|
||||
..write('origin: $origin, ')
|
||||
..write('iconData: $iconData, ')
|
||||
..write('fetchDate: $fetchDate, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Onboarding extends Table with TableInfo<Onboarding, OnboardingData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Onboarding(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<int> revision = GeneratedColumn<int>(
|
||||
'revision',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<DateTime> completionDate =
|
||||
GeneratedColumn<DateTime>(
|
||||
'completion_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [revision, completionDate];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'onboarding';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
@override
|
||||
OnboardingData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return OnboardingData(
|
||||
revision: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}revision'],
|
||||
)!,
|
||||
completionDate: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}completion_date'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Onboarding createAlias(String alias) {
|
||||
return Onboarding(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class OnboardingData extends DataClass implements Insertable<OnboardingData> {
|
||||
final int revision;
|
||||
final DateTime completionDate;
|
||||
const OnboardingData({required this.revision, required this.completionDate});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['revision'] = Variable<int>(revision);
|
||||
map['completion_date'] = Variable<DateTime>(completionDate);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory OnboardingData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return OnboardingData(
|
||||
revision: serializer.fromJson<int>(json['revision']),
|
||||
completionDate: serializer.fromJson<DateTime>(json['completionDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'revision': serializer.toJson<int>(revision),
|
||||
'completionDate': serializer.toJson<DateTime>(completionDate),
|
||||
};
|
||||
}
|
||||
|
||||
OnboardingData copyWith({int? revision, DateTime? completionDate}) =>
|
||||
OnboardingData(
|
||||
revision: revision ?? this.revision,
|
||||
completionDate: completionDate ?? this.completionDate,
|
||||
);
|
||||
OnboardingData copyWithCompanion(OnboardingCompanion data) {
|
||||
return OnboardingData(
|
||||
revision: data.revision.present ? data.revision.value : this.revision,
|
||||
completionDate: data.completionDate.present
|
||||
? data.completionDate.value
|
||||
: this.completionDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnboardingData(')
|
||||
..write('revision: $revision, ')
|
||||
..write('completionDate: $completionDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(revision, completionDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is OnboardingData &&
|
||||
other.revision == this.revision &&
|
||||
other.completionDate == this.completionDate);
|
||||
}
|
||||
|
||||
class OnboardingCompanion extends UpdateCompanion<OnboardingData> {
|
||||
final Value<int> revision;
|
||||
final Value<DateTime> completionDate;
|
||||
final Value<int> rowid;
|
||||
const OnboardingCompanion({
|
||||
this.revision = const Value.absent(),
|
||||
this.completionDate = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
OnboardingCompanion.insert({
|
||||
required int revision,
|
||||
required DateTime completionDate,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : revision = Value(revision),
|
||||
completionDate = Value(completionDate);
|
||||
static Insertable<OnboardingData> custom({
|
||||
Expression<int>? revision,
|
||||
Expression<DateTime>? completionDate,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (revision != null) 'revision': revision,
|
||||
if (completionDate != null) 'completion_date': completionDate,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
OnboardingCompanion copyWith({
|
||||
Value<int>? revision,
|
||||
Value<DateTime>? completionDate,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return OnboardingCompanion(
|
||||
revision: revision ?? this.revision,
|
||||
completionDate: completionDate ?? this.completionDate,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (revision.present) {
|
||||
map['revision'] = Variable<int>(revision.value);
|
||||
}
|
||||
if (completionDate.present) {
|
||||
map['completion_date'] = Variable<DateTime>(completionDate.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnboardingCompanion(')
|
||||
..write('revision: $revision, ')
|
||||
..write('completionDate: $completionDate, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV1 extends GeneratedDatabase {
|
||||
DatabaseAtV1(QueryExecutor e) : super(e);
|
||||
late final Setting setting = Setting(this);
|
||||
late final IconCache iconCache = IconCache(this);
|
||||
late final Onboarding onboarding = Onboarding(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
setting,
|
||||
iconCache,
|
||||
onboarding,
|
||||
];
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
// dart format width=80
|
||||
import 'dart:typed_data' as i2;
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Setting extends Table with TableInfo<Setting, SettingData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Setting(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> key = GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> partitionKey = GeneratedColumn<String>(
|
||||
'partition_key',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
late final GeneratedColumn<DriftAny> value = GeneratedColumn<DriftAny>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.any,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [key, partitionKey, value];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'setting';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
SettingData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return SettingData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
partitionKey: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}partition_key'],
|
||||
),
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.any,
|
||||
data['${effectivePrefix}value'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Setting createAlias(String alias) {
|
||||
return Setting(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class SettingData extends DataClass implements Insertable<SettingData> {
|
||||
final String key;
|
||||
final String? partitionKey;
|
||||
final DriftAny? value;
|
||||
const SettingData({required this.key, this.partitionKey, this.value});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['key'] = Variable<String>(key);
|
||||
if (!nullToAbsent || partitionKey != null) {
|
||||
map['partition_key'] = Variable<String>(partitionKey);
|
||||
}
|
||||
if (!nullToAbsent || value != null) {
|
||||
map['value'] = Variable<DriftAny>(value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory SettingData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return SettingData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
partitionKey: serializer.fromJson<String?>(json['partitionKey']),
|
||||
value: serializer.fromJson<DriftAny?>(json['value']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'partitionKey': serializer.toJson<String?>(partitionKey),
|
||||
'value': serializer.toJson<DriftAny?>(value),
|
||||
};
|
||||
}
|
||||
|
||||
SettingData copyWith({
|
||||
String? key,
|
||||
Value<String?> partitionKey = const Value.absent(),
|
||||
Value<DriftAny?> value = const Value.absent(),
|
||||
}) => SettingData(
|
||||
key: key ?? this.key,
|
||||
partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey,
|
||||
value: value.present ? value.value : this.value,
|
||||
);
|
||||
SettingData copyWithCompanion(SettingCompanion data) {
|
||||
return SettingData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
partitionKey: data.partitionKey.present
|
||||
? data.partitionKey.value
|
||||
: this.partitionKey,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SettingData(')
|
||||
..write('key: $key, ')
|
||||
..write('partitionKey: $partitionKey, ')
|
||||
..write('value: $value')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, partitionKey, value);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is SettingData &&
|
||||
other.key == this.key &&
|
||||
other.partitionKey == this.partitionKey &&
|
||||
other.value == this.value);
|
||||
}
|
||||
|
||||
class SettingCompanion extends UpdateCompanion<SettingData> {
|
||||
final Value<String> key;
|
||||
final Value<String?> partitionKey;
|
||||
final Value<DriftAny?> value;
|
||||
final Value<int> rowid;
|
||||
const SettingCompanion({
|
||||
this.key = const Value.absent(),
|
||||
this.partitionKey = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
SettingCompanion.insert({
|
||||
required String key,
|
||||
this.partitionKey = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : key = Value(key);
|
||||
static Insertable<SettingData> custom({
|
||||
Expression<String>? key,
|
||||
Expression<String>? partitionKey,
|
||||
Expression<DriftAny>? value,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (key != null) 'key': key,
|
||||
if (partitionKey != null) 'partition_key': partitionKey,
|
||||
if (value != null) 'value': value,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
SettingCompanion copyWith({
|
||||
Value<String>? key,
|
||||
Value<String?>? partitionKey,
|
||||
Value<DriftAny?>? value,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return SettingCompanion(
|
||||
key: key ?? this.key,
|
||||
partitionKey: partitionKey ?? this.partitionKey,
|
||||
value: value ?? this.value,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (key.present) {
|
||||
map['key'] = Variable<String>(key.value);
|
||||
}
|
||||
if (partitionKey.present) {
|
||||
map['partition_key'] = Variable<String>(partitionKey.value);
|
||||
}
|
||||
if (value.present) {
|
||||
map['value'] = Variable<DriftAny>(value.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SettingCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('partitionKey: $partitionKey, ')
|
||||
..write('value: $value, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class IconCache extends Table with TableInfo<IconCache, IconCacheData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
IconCache(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> origin = GeneratedColumn<String>(
|
||||
'origin',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<i2.Uint8List> iconData =
|
||||
GeneratedColumn<i2.Uint8List>(
|
||||
'icon_data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> fetchDate = GeneratedColumn<int>(
|
||||
'fetch_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [origin, iconData, fetchDate];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'icon_cache';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {origin};
|
||||
@override
|
||||
IconCacheData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return IconCacheData(
|
||||
origin: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}origin'],
|
||||
)!,
|
||||
iconData: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.blob,
|
||||
data['${effectivePrefix}icon_data'],
|
||||
)!,
|
||||
fetchDate: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}fetch_date'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconCache createAlias(String alias) {
|
||||
return IconCache(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class IconCacheData extends DataClass implements Insertable<IconCacheData> {
|
||||
final String origin;
|
||||
final i2.Uint8List iconData;
|
||||
final int fetchDate;
|
||||
const IconCacheData({
|
||||
required this.origin,
|
||||
required this.iconData,
|
||||
required this.fetchDate,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['origin'] = Variable<String>(origin);
|
||||
map['icon_data'] = Variable<i2.Uint8List>(iconData);
|
||||
map['fetch_date'] = Variable<int>(fetchDate);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory IconCacheData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return IconCacheData(
|
||||
origin: serializer.fromJson<String>(json['origin']),
|
||||
iconData: serializer.fromJson<i2.Uint8List>(json['iconData']),
|
||||
fetchDate: serializer.fromJson<int>(json['fetchDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'origin': serializer.toJson<String>(origin),
|
||||
'iconData': serializer.toJson<i2.Uint8List>(iconData),
|
||||
'fetchDate': serializer.toJson<int>(fetchDate),
|
||||
};
|
||||
}
|
||||
|
||||
IconCacheData copyWith({
|
||||
String? origin,
|
||||
i2.Uint8List? iconData,
|
||||
int? fetchDate,
|
||||
}) => IconCacheData(
|
||||
origin: origin ?? this.origin,
|
||||
iconData: iconData ?? this.iconData,
|
||||
fetchDate: fetchDate ?? this.fetchDate,
|
||||
);
|
||||
IconCacheData copyWithCompanion(IconCacheCompanion data) {
|
||||
return IconCacheData(
|
||||
origin: data.origin.present ? data.origin.value : this.origin,
|
||||
iconData: data.iconData.present ? data.iconData.value : this.iconData,
|
||||
fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('IconCacheData(')
|
||||
..write('origin: $origin, ')
|
||||
..write('iconData: $iconData, ')
|
||||
..write('fetchDate: $fetchDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is IconCacheData &&
|
||||
other.origin == this.origin &&
|
||||
$driftBlobEquality.equals(other.iconData, this.iconData) &&
|
||||
other.fetchDate == this.fetchDate);
|
||||
}
|
||||
|
||||
class IconCacheCompanion extends UpdateCompanion<IconCacheData> {
|
||||
final Value<String> origin;
|
||||
final Value<i2.Uint8List> iconData;
|
||||
final Value<int> fetchDate;
|
||||
final Value<int> rowid;
|
||||
const IconCacheCompanion({
|
||||
this.origin = const Value.absent(),
|
||||
this.iconData = const Value.absent(),
|
||||
this.fetchDate = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
IconCacheCompanion.insert({
|
||||
required String origin,
|
||||
required i2.Uint8List iconData,
|
||||
required int fetchDate,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : origin = Value(origin),
|
||||
iconData = Value(iconData),
|
||||
fetchDate = Value(fetchDate);
|
||||
static Insertable<IconCacheData> custom({
|
||||
Expression<String>? origin,
|
||||
Expression<i2.Uint8List>? iconData,
|
||||
Expression<int>? fetchDate,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (origin != null) 'origin': origin,
|
||||
if (iconData != null) 'icon_data': iconData,
|
||||
if (fetchDate != null) 'fetch_date': fetchDate,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
IconCacheCompanion copyWith({
|
||||
Value<String>? origin,
|
||||
Value<i2.Uint8List>? iconData,
|
||||
Value<int>? fetchDate,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return IconCacheCompanion(
|
||||
origin: origin ?? this.origin,
|
||||
iconData: iconData ?? this.iconData,
|
||||
fetchDate: fetchDate ?? this.fetchDate,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (origin.present) {
|
||||
map['origin'] = Variable<String>(origin.value);
|
||||
}
|
||||
if (iconData.present) {
|
||||
map['icon_data'] = Variable<i2.Uint8List>(iconData.value);
|
||||
}
|
||||
if (fetchDate.present) {
|
||||
map['fetch_date'] = Variable<int>(fetchDate.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('IconCacheCompanion(')
|
||||
..write('origin: $origin, ')
|
||||
..write('iconData: $iconData, ')
|
||||
..write('fetchDate: $fetchDate, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Onboarding extends Table with TableInfo<Onboarding, OnboardingData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Onboarding(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<int> revision = GeneratedColumn<int>(
|
||||
'revision',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> completionDate = GeneratedColumn<int>(
|
||||
'completion_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [revision, completionDate];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'onboarding';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
@override
|
||||
OnboardingData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return OnboardingData(
|
||||
revision: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}revision'],
|
||||
)!,
|
||||
completionDate: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}completion_date'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Onboarding createAlias(String alias) {
|
||||
return Onboarding(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class OnboardingData extends DataClass implements Insertable<OnboardingData> {
|
||||
final int revision;
|
||||
final int completionDate;
|
||||
const OnboardingData({required this.revision, required this.completionDate});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['revision'] = Variable<int>(revision);
|
||||
map['completion_date'] = Variable<int>(completionDate);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory OnboardingData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return OnboardingData(
|
||||
revision: serializer.fromJson<int>(json['revision']),
|
||||
completionDate: serializer.fromJson<int>(json['completionDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'revision': serializer.toJson<int>(revision),
|
||||
'completionDate': serializer.toJson<int>(completionDate),
|
||||
};
|
||||
}
|
||||
|
||||
OnboardingData copyWith({int? revision, int? completionDate}) =>
|
||||
OnboardingData(
|
||||
revision: revision ?? this.revision,
|
||||
completionDate: completionDate ?? this.completionDate,
|
||||
);
|
||||
OnboardingData copyWithCompanion(OnboardingCompanion data) {
|
||||
return OnboardingData(
|
||||
revision: data.revision.present ? data.revision.value : this.revision,
|
||||
completionDate: data.completionDate.present
|
||||
? data.completionDate.value
|
||||
: this.completionDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnboardingData(')
|
||||
..write('revision: $revision, ')
|
||||
..write('completionDate: $completionDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(revision, completionDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is OnboardingData &&
|
||||
other.revision == this.revision &&
|
||||
other.completionDate == this.completionDate);
|
||||
}
|
||||
|
||||
class OnboardingCompanion extends UpdateCompanion<OnboardingData> {
|
||||
final Value<int> revision;
|
||||
final Value<int> completionDate;
|
||||
final Value<int> rowid;
|
||||
const OnboardingCompanion({
|
||||
this.revision = const Value.absent(),
|
||||
this.completionDate = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
OnboardingCompanion.insert({
|
||||
required int revision,
|
||||
required int completionDate,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : revision = Value(revision),
|
||||
completionDate = Value(completionDate);
|
||||
static Insertable<OnboardingData> custom({
|
||||
Expression<int>? revision,
|
||||
Expression<int>? completionDate,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (revision != null) 'revision': revision,
|
||||
if (completionDate != null) 'completion_date': completionDate,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
OnboardingCompanion copyWith({
|
||||
Value<int>? revision,
|
||||
Value<int>? completionDate,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return OnboardingCompanion(
|
||||
revision: revision ?? this.revision,
|
||||
completionDate: completionDate ?? this.completionDate,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (revision.present) {
|
||||
map['revision'] = Variable<int>(revision.value);
|
||||
}
|
||||
if (completionDate.present) {
|
||||
map['completion_date'] = Variable<int>(completionDate.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnboardingCompanion(')
|
||||
..write('revision: $revision, ')
|
||||
..write('completionDate: $completionDate, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Riverpod extends Table with TableInfo<Riverpod, RiverpodData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Riverpod(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> key = GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> json = GeneratedColumn<String>(
|
||||
'json',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> expireAt = GeneratedColumn<int>(
|
||||
'expireAt',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
late final GeneratedColumn<String> destroyKey = GeneratedColumn<String>(
|
||||
'destroyKey',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [key, json, expireAt, destroyKey];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'riverpod';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
RiverpodData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return RiverpodData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
json: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}json'],
|
||||
)!,
|
||||
expireAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}expireAt'],
|
||||
),
|
||||
destroyKey: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}destroyKey'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Riverpod createAlias(String alias) {
|
||||
return Riverpod(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class RiverpodData extends DataClass implements Insertable<RiverpodData> {
|
||||
final String key;
|
||||
final String json;
|
||||
final int? expireAt;
|
||||
final String? destroyKey;
|
||||
const RiverpodData({
|
||||
required this.key,
|
||||
required this.json,
|
||||
this.expireAt,
|
||||
this.destroyKey,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['key'] = Variable<String>(key);
|
||||
map['json'] = Variable<String>(json);
|
||||
if (!nullToAbsent || expireAt != null) {
|
||||
map['expireAt'] = Variable<int>(expireAt);
|
||||
}
|
||||
if (!nullToAbsent || destroyKey != null) {
|
||||
map['destroyKey'] = Variable<String>(destroyKey);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory RiverpodData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return RiverpodData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
json: serializer.fromJson<String>(json['json']),
|
||||
expireAt: serializer.fromJson<int?>(json['expireAt']),
|
||||
destroyKey: serializer.fromJson<String?>(json['destroyKey']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'json': serializer.toJson<String>(json),
|
||||
'expireAt': serializer.toJson<int?>(expireAt),
|
||||
'destroyKey': serializer.toJson<String?>(destroyKey),
|
||||
};
|
||||
}
|
||||
|
||||
RiverpodData copyWith({
|
||||
String? key,
|
||||
String? json,
|
||||
Value<int?> expireAt = const Value.absent(),
|
||||
Value<String?> destroyKey = const Value.absent(),
|
||||
}) => RiverpodData(
|
||||
key: key ?? this.key,
|
||||
json: json ?? this.json,
|
||||
expireAt: expireAt.present ? expireAt.value : this.expireAt,
|
||||
destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey,
|
||||
);
|
||||
RiverpodData copyWithCompanion(RiverpodCompanion data) {
|
||||
return RiverpodData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
json: data.json.present ? data.json.value : this.json,
|
||||
expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt,
|
||||
destroyKey: data.destroyKey.present
|
||||
? data.destroyKey.value
|
||||
: this.destroyKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('RiverpodData(')
|
||||
..write('key: $key, ')
|
||||
..write('json: $json, ')
|
||||
..write('expireAt: $expireAt, ')
|
||||
..write('destroyKey: $destroyKey')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, json, expireAt, destroyKey);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is RiverpodData &&
|
||||
other.key == this.key &&
|
||||
other.json == this.json &&
|
||||
other.expireAt == this.expireAt &&
|
||||
other.destroyKey == this.destroyKey);
|
||||
}
|
||||
|
||||
class RiverpodCompanion extends UpdateCompanion<RiverpodData> {
|
||||
final Value<String> key;
|
||||
final Value<String> json;
|
||||
final Value<int?> expireAt;
|
||||
final Value<String?> destroyKey;
|
||||
const RiverpodCompanion({
|
||||
this.key = const Value.absent(),
|
||||
this.json = const Value.absent(),
|
||||
this.expireAt = const Value.absent(),
|
||||
this.destroyKey = const Value.absent(),
|
||||
});
|
||||
RiverpodCompanion.insert({
|
||||
required String key,
|
||||
required String json,
|
||||
this.expireAt = const Value.absent(),
|
||||
this.destroyKey = const Value.absent(),
|
||||
}) : key = Value(key),
|
||||
json = Value(json);
|
||||
static Insertable<RiverpodData> custom({
|
||||
Expression<String>? key,
|
||||
Expression<String>? json,
|
||||
Expression<int>? expireAt,
|
||||
Expression<String>? destroyKey,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (key != null) 'key': key,
|
||||
if (json != null) 'json': json,
|
||||
if (expireAt != null) 'expireAt': expireAt,
|
||||
if (destroyKey != null) 'destroyKey': destroyKey,
|
||||
});
|
||||
}
|
||||
|
||||
RiverpodCompanion copyWith({
|
||||
Value<String>? key,
|
||||
Value<String>? json,
|
||||
Value<int?>? expireAt,
|
||||
Value<String?>? destroyKey,
|
||||
}) {
|
||||
return RiverpodCompanion(
|
||||
key: key ?? this.key,
|
||||
json: json ?? this.json,
|
||||
expireAt: expireAt ?? this.expireAt,
|
||||
destroyKey: destroyKey ?? this.destroyKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (key.present) {
|
||||
map['key'] = Variable<String>(key.value);
|
||||
}
|
||||
if (json.present) {
|
||||
map['json'] = Variable<String>(json.value);
|
||||
}
|
||||
if (expireAt.present) {
|
||||
map['expireAt'] = Variable<int>(expireAt.value);
|
||||
}
|
||||
if (destroyKey.present) {
|
||||
map['destroyKey'] = Variable<String>(destroyKey.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('RiverpodCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('json: $json, ')
|
||||
..write('expireAt: $expireAt, ')
|
||||
..write('destroyKey: $destroyKey')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV2 extends GeneratedDatabase {
|
||||
DatabaseAtV2(QueryExecutor e) : super(e);
|
||||
late final Setting setting = Setting(this);
|
||||
late final IconCache iconCache = IconCache(this);
|
||||
late final Onboarding onboarding = Onboarding(this);
|
||||
late final Riverpod riverpod = Riverpod(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
setting,
|
||||
iconCache,
|
||||
onboarding,
|
||||
riverpod,
|
||||
];
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v1.dart' as v1;
|
||||
import 'generated/schema_v2.dart' as v2;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
// These simple tests verify all possible schema updates with a simple (no
|
||||
// data) migration. This is a quick way to ensure that written database
|
||||
// migrations properly alter the schema.
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = UserDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The following template shows how to write tests ensuring your migrations
|
||||
// preserve existing data.
|
||||
// Testing this can be useful for migrations that change existing columns
|
||||
// (e.g. by alterating their type or constraints). Migrations that only add
|
||||
// tables or columns typically don't need these advanced tests. For more
|
||||
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
|
||||
// TODO: This generated template shows how these tests could be written. Adopt
|
||||
// it to your own needs when testing migrations with data integrity.
|
||||
test('migration from v1 to v2 does not corrupt data', () async {
|
||||
// Add data to insert into the old database, and the expected rows after the
|
||||
// migration.
|
||||
// TODO: Fill these lists
|
||||
final oldSettingData = <v1.SettingData>[];
|
||||
final expectedNewSettingData = <v2.SettingData>[];
|
||||
|
||||
final oldIconCacheData = <v1.IconCacheData>[];
|
||||
final expectedNewIconCacheData = <v2.IconCacheData>[];
|
||||
|
||||
final oldOnboardingData = <v1.OnboardingData>[];
|
||||
final expectedNewOnboardingData = <v2.OnboardingData>[];
|
||||
|
||||
await verifier.testWithDataIntegrity(
|
||||
oldVersion: 1,
|
||||
newVersion: 2,
|
||||
createOld: v1.DatabaseAtV1.new,
|
||||
createNew: v2.DatabaseAtV2.new,
|
||||
openTestedDatabase: UserDatabase.new,
|
||||
createItems: (batch, oldDb) {
|
||||
batch.insertAll(oldDb.setting, oldSettingData);
|
||||
batch.insertAll(oldDb.iconCache, oldIconCacheData);
|
||||
batch.insertAll(oldDb.onboarding, oldOnboardingData);
|
||||
},
|
||||
validateItems: (newDb) async {
|
||||
expect(expectedNewSettingData, await newDb.select(newDb.setting).get());
|
||||
expect(
|
||||
expectedNewIconCacheData,
|
||||
await newDb.select(newDb.iconCache).get(),
|
||||
);
|
||||
expect(
|
||||
expectedNewOnboardingData,
|
||||
await newDb.select(newDb.onboarding).get(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/providers/toolbar_button_configs.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
|
||||
void main() {
|
||||
late UserDatabase db;
|
||||
|
||||
setUp(() {
|
||||
db = UserDatabase(
|
||||
NativeDatabase.memory(
|
||||
setup: (database) {
|
||||
registerLexorankFunctions(database);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('ToolbarButtonConfigDao', () {
|
||||
test(
|
||||
'seedMissing inserts self-referential fallback rows in two phases',
|
||||
() async {
|
||||
await db.toolbarButtonConfigDao.seedMissing([
|
||||
(
|
||||
buttonId: 'back',
|
||||
defaultVisible: true,
|
||||
defaultFallback: 'bookmarks',
|
||||
),
|
||||
(buttonId: 'bookmarks', defaultVisible: false, defaultFallback: null),
|
||||
(buttonId: 'forward', defaultVisible: true, defaultFallback: 'share'),
|
||||
(buttonId: 'share', defaultVisible: false, defaultFallback: null),
|
||||
]);
|
||||
|
||||
final configs = await db.toolbarButtonConfigDao.getAll();
|
||||
final configsById = {
|
||||
for (final config in configs) config.buttonId: config,
|
||||
};
|
||||
|
||||
expect(configsById['back']?.fallbackId, 'bookmarks');
|
||||
expect(configsById['forward']?.fallbackId, 'share');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'replaceAll persists default toolbar fallbacks without FK failures',
|
||||
() async {
|
||||
await expectLater(
|
||||
db.toolbarButtonConfigDao.replaceAll(
|
||||
buildDefaultToolbarButtonConfigs(),
|
||||
),
|
||||
completes,
|
||||
);
|
||||
|
||||
final configs = await db.toolbarButtonConfigDao.getAll();
|
||||
final configsById = {
|
||||
for (final config in configs) config.buttonId: config,
|
||||
};
|
||||
|
||||
expect(configsById['back']?.fallbackId, 'bookmarks');
|
||||
expect(configsById['forward']?.fallbackId, 'share');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+766
@@ -0,0 +1,766 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_html_utils.dart';
|
||||
|
||||
@GenerateMocks([GeckoBookmarksService])
|
||||
import 'bookmark_html_utils_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGeckoBookmarksService mockService;
|
||||
late BookmarkHTMLUtils utils;
|
||||
|
||||
setUp(() {
|
||||
mockService = MockGeckoBookmarksService();
|
||||
utils = BookmarkHTMLUtils(mockService);
|
||||
});
|
||||
|
||||
group('BookmarkHTMLUtils - Import', () {
|
||||
test('should handle corrupt HTML file with malformed URIs', () async {
|
||||
// Load the corrupt fixture
|
||||
final fixtureFile = File(
|
||||
'test/utils/bookmarks/fixtures/bookmarks.corrupt.html',
|
||||
);
|
||||
final htmlString = await fixtureFile.readAsString();
|
||||
|
||||
// Mock the service calls
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'generated_guid');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'generated_guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlString, replace: true);
|
||||
|
||||
// Should import valid bookmarks and skip the corrupt one
|
||||
expect(count, greaterThan(0));
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
|
||||
test('should import from valid HTML file', () async {
|
||||
final fixtureFile = File(
|
||||
'test/utils/bookmarks/fixtures/bookmarks.preplaces.html',
|
||||
);
|
||||
final htmlString = await fixtureFile.readAsString();
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder_guid');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark_guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlString, replace: true);
|
||||
|
||||
expect(count, greaterThan(0));
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
// Verify some bookmarks were added
|
||||
verify(mockService.addItem(any, any, any, any)).called(greaterThan(0));
|
||||
});
|
||||
|
||||
test('should handle empty HTML', () async {
|
||||
const emptyHtml = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
|
||||
final count = await utils.importFromHTML(emptyHtml, replace: true);
|
||||
|
||||
expect(count, equals(0));
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
|
||||
test('should not erase when replace is false', () async {
|
||||
const simpleHtml = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com">Example</A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
await utils.importFromHTML(simpleHtml);
|
||||
|
||||
verifyNever(mockService.eraseEverything(any));
|
||||
});
|
||||
|
||||
test('should handle bookmarks with special characters in title', () async {
|
||||
const htmlWithSpecialChars = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com"><unescaped="test"></A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithSpecialChars);
|
||||
|
||||
expect(count, equals(1));
|
||||
final captured = verify(
|
||||
mockService.addItem(any, any, captureAny, any),
|
||||
).captured;
|
||||
// Should properly decode HTML entities
|
||||
expect(captured[0], equals('<unescaped="test">'));
|
||||
});
|
||||
|
||||
test('should import bookmarks with timestamps', () async {
|
||||
const htmlWithDates = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com" ADD_DATE="1177375336" LAST_MODIFIED="1177375423">Test</A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithDates);
|
||||
|
||||
expect(count, equals(1));
|
||||
});
|
||||
|
||||
test('should handle folder hierarchy', () async {
|
||||
const htmlWithFolders = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><H3>Parent Folder</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com/1">Child 1</A>
|
||||
<DT><H3>Nested Folder</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com/2">Grandchild</A>
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder_guid');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark_guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithFolders);
|
||||
|
||||
expect(count, equals(2)); // 2 bookmarks
|
||||
verify(mockService.addFolder(any, any, any)).called(2); // 2 folders
|
||||
});
|
||||
|
||||
test('should recognize toolbar folder', () async {
|
||||
const htmlWithToolbar = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><H3 PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com">Toolbar Bookmark</A>
|
||||
</DL><p>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
await utils.importFromHTML(htmlWithToolbar, replace: true);
|
||||
|
||||
// When replace is true, should add to toolbar
|
||||
final captured = verify(
|
||||
mockService.addItem(captureAny, any, any, any),
|
||||
).captured;
|
||||
expect(captured[0], equals(BookmarkRoot.toolbar.id));
|
||||
});
|
||||
|
||||
test('should recognize unfiled folder', () async {
|
||||
const htmlWithUnfiled = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com">Unfiled Bookmark</A>
|
||||
</DL><p>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
await utils.importFromHTML(htmlWithUnfiled, replace: true);
|
||||
|
||||
final captured = verify(
|
||||
mockService.addItem(captureAny, any, any, any),
|
||||
).captured;
|
||||
expect(captured[0], equals(BookmarkRoot.unfiled.id));
|
||||
});
|
||||
|
||||
test('should handle separators', () async {
|
||||
const htmlWithSeparator = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A HREF="https://example.com/1">Bookmark 1</A>
|
||||
<HR>
|
||||
<DT><A HREF="https://example.com/2">Bookmark 2</A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithSeparator);
|
||||
|
||||
// Should import 2 bookmarks (separator is not supported by Android API)
|
||||
expect(count, equals(2));
|
||||
});
|
||||
|
||||
test('should skip bookmarks without URLs', () async {
|
||||
const htmlWithoutUrl = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A>No URL</A>
|
||||
<DT><A HREF="https://example.com">Valid</A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithoutUrl);
|
||||
|
||||
expect(count, equals(1)); // Only the valid one
|
||||
});
|
||||
|
||||
test('should skip bookmarks with invalid URLs', () async {
|
||||
const htmlWithInvalidUrl = '''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
<DT><A HREF="not a url">Invalid</A>
|
||||
<DT><A HREF="https://example.com">Valid</A>
|
||||
</DL>
|
||||
''';
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlWithInvalidUrl);
|
||||
|
||||
expect(count, equals(1));
|
||||
});
|
||||
|
||||
test('should handle single frame HTML', () async {
|
||||
final fixtureFile = File(
|
||||
'test/utils/bookmarks/fixtures/bookmarks_html_singleframe.html',
|
||||
);
|
||||
final htmlString = await fixtureFile.readAsString();
|
||||
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder_guid');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark_guid');
|
||||
|
||||
final count = await utils.importFromHTML(htmlString);
|
||||
|
||||
expect(count, greaterThan(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('BookmarkHTMLUtils - Export', () {
|
||||
test('should export bookmark tree to HTML', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
expect(html, contains('<!DOCTYPE NETSCAPE-Bookmark-file-1>'));
|
||||
expect(html, contains('<H1>Bookmarks Menu</H1>'));
|
||||
expect(html, contains('https://example.com'));
|
||||
expect(html, contains('Test Bookmark'));
|
||||
});
|
||||
|
||||
test('should escape HTML entities in export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: '<unescaped="test">',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
// Should escape special characters
|
||||
expect(html, contains('<unescaped="test">'));
|
||||
expect(html, isNot(contains('<unescaped="test">')));
|
||||
});
|
||||
|
||||
test('should include date attributes in export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Test',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
expect(html, contains('ADD_DATE='));
|
||||
expect(html, contains('LAST_MODIFIED='));
|
||||
});
|
||||
|
||||
test('should export toolbar with title as H1 when root', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.toolbar.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks Toolbar',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.toolbar);
|
||||
|
||||
// When toolbar is the root, it becomes H1 without special attributes
|
||||
expect(html, contains('<H1>Bookmarks Toolbar</H1>'));
|
||||
expect(html, isNot(contains('PERSONAL_TOOLBAR_FOLDER')));
|
||||
});
|
||||
|
||||
test('should export unfiled with title as H1 when root', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.unfiled.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Unsorted Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.unfiled);
|
||||
|
||||
// When unfiled is the root, it becomes H1 without special attributes
|
||||
expect(html, contains('<H1>Unsorted Bookmarks</H1>'));
|
||||
expect(html, isNot(contains('UNFILED_BOOKMARKS_FOLDER')));
|
||||
});
|
||||
|
||||
test('should export separators', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'First',
|
||||
url: 'https://example.com/1',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'separator___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 1,
|
||||
title: null,
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.separator,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'bookmark2___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 2,
|
||||
title: 'Second',
|
||||
url: 'https://example.com/2',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
expect(html, contains('<HR>'));
|
||||
});
|
||||
|
||||
test('should skip bookmarks with invalid URLs during export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'invalid1____',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Invalid',
|
||||
url: '', // Empty URL
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'valid1______',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 1,
|
||||
title: 'Valid',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
// Should only contain the valid bookmark
|
||||
expect(html, contains('https://example.com'));
|
||||
expect(html, contains('Valid'));
|
||||
expect(html, isNot(contains('Invalid')));
|
||||
});
|
||||
|
||||
test('should export nested folders', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'folder1_____',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Parent Folder',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'folder1_____',
|
||||
position: 0,
|
||||
title: 'Nested Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
expect(html, contains('Parent Folder'));
|
||||
expect(html, contains('Nested Bookmark'));
|
||||
expect(html, contains('<H3'));
|
||||
expect(html, contains('</H3>'));
|
||||
});
|
||||
|
||||
test('should throw when tree cannot be fetched', () {
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => null);
|
||||
|
||||
expect(
|
||||
() => utils.exportToHTML(root: BookmarkRoot.menu),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('should include proper HTML header', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
expect(html, contains('<!DOCTYPE NETSCAPE-Bookmark-file-1>'));
|
||||
expect(html, contains('<META HTTP-EQUIV="Content-Type"'));
|
||||
expect(html, contains('<TITLE>Bookmarks</TITLE>'));
|
||||
expect(html, contains('Content-Security-Policy'));
|
||||
});
|
||||
|
||||
test('should properly indent HTML structure', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks',
|
||||
url: null,
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Test',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 0,
|
||||
lastModified: 0,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
|
||||
// Should contain indentation
|
||||
expect(html, contains(' <DT>'));
|
||||
expect(html, contains('<DL><p>'));
|
||||
expect(html, contains('</DL>'));
|
||||
});
|
||||
});
|
||||
|
||||
group('BookmarkHTMLUtils - Import/Export Round-Trip', () {
|
||||
test('should preserve data through export and re-import', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: BookmarkRoot.menu.id,
|
||||
parentGuid: BookmarkRoot.root.id,
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'folder1_____',
|
||||
parentGuid: BookmarkRoot.menu.id,
|
||||
position: 0,
|
||||
title: 'Test Folder',
|
||||
url: null,
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'folder1_____',
|
||||
position: 0,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
// Export
|
||||
final html = await utils.exportToHTML(root: BookmarkRoot.menu);
|
||||
expect(html, isNotEmpty);
|
||||
|
||||
// Re-import
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder1_____');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark1___');
|
||||
|
||||
final count = await utils.importFromHTML(html, replace: true);
|
||||
|
||||
expect(count, equals(1)); // One bookmark imported
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in weblibre/test/features/geckoview/features/bookmarks/utils/bookmark_html_utils_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i3;
|
||||
|
||||
import 'package:flutter_mozilla_components/src/domain/services/gecko_bookmarks.dart'
|
||||
as _i2;
|
||||
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart' as _i4;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i5;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
/// A class which mocks [GeckoBookmarksService].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGeckoBookmarksService extends _i1.Mock
|
||||
implements _i2.GeckoBookmarksService {
|
||||
MockGeckoBookmarksService() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i3.Future<_i4.BookmarkNode?> getTree(
|
||||
String? guid, {
|
||||
bool? recursive = false,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getTree, [guid], {#recursive: recursive}),
|
||||
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
|
||||
)
|
||||
as _i3.Future<_i4.BookmarkNode?>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i4.BookmarkNode?> getBookmark(String? guid) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getBookmark, [guid]),
|
||||
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
|
||||
)
|
||||
as _i3.Future<_i4.BookmarkNode?>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> getBookmarksWithUrl(Uri? url) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getBookmarksWithUrl, [url]),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> getRecentBookmarks(
|
||||
int? limit, {
|
||||
Duration? maxAge = Duration.zero,
|
||||
DateTime? currentTime,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getRecentBookmarks,
|
||||
[limit],
|
||||
{#maxAge: maxAge, #currentTime: currentTime},
|
||||
),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> searchBookmarks(
|
||||
String? query, {
|
||||
int? limit = 10,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#searchBookmarks, [query], {#limit: limit}),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<String> addItem(
|
||||
String? parentGuid,
|
||||
Uri? url,
|
||||
String? title,
|
||||
int? position,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addItem, [parentGuid, url, title, position]),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#addItem, [parentGuid, url, title, position]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<String> addFolder(
|
||||
String? parentGuid,
|
||||
String? title,
|
||||
int? position,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addFolder, [parentGuid, title, position]),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#addFolder, [parentGuid, title, position]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<void> updateNode(String? guid, _i4.BookmarkInfo? info) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateNode, [guid, info]),
|
||||
returnValue: _i3.Future<void>.value(),
|
||||
returnValueForMissingStub: _i3.Future<void>.value(),
|
||||
)
|
||||
as _i3.Future<void>);
|
||||
|
||||
@override
|
||||
_i3.Future<bool> deleteNode(String? guid) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteNode, [guid]),
|
||||
returnValue: _i3.Future<bool>.value(false),
|
||||
)
|
||||
as _i3.Future<bool>);
|
||||
|
||||
@override
|
||||
_i3.Future<void> eraseEverything(_i2.BookmarkRoot? root) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#eraseEverything, [root]),
|
||||
returnValue: _i3.Future<void>.value(),
|
||||
returnValueForMissingStub: _i3.Future<void>.value(),
|
||||
)
|
||||
as _i3.Future<void>);
|
||||
}
|
||||
+796
@@ -0,0 +1,796 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ignore_for_file: avoid_redundant_argument_values, avoid_dynamic_calls
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_json_utils.dart';
|
||||
|
||||
@GenerateMocks([GeckoBookmarksService])
|
||||
import 'bookmark_json_utils_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGeckoBookmarksService mockService;
|
||||
late BookmarkJSONUtils utils;
|
||||
|
||||
setUp(() {
|
||||
mockService = MockGeckoBookmarksService();
|
||||
utils = BookmarkJSONUtils(mockService);
|
||||
});
|
||||
|
||||
group('BookmarkJSONUtils - Import', () {
|
||||
test('should reject invalid JSON format', () {
|
||||
const invalidJson = '[]';
|
||||
|
||||
expect(
|
||||
() => utils.importFromJSON(invalidJson),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return 0 for empty children', () async {
|
||||
const emptyJson = '{"children": []}';
|
||||
|
||||
final count = await utils.importFromJSON(emptyJson);
|
||||
|
||||
expect(count, equals(0));
|
||||
});
|
||||
|
||||
test('should return 0 when children is null', () async {
|
||||
const noChildrenJson = '{"guid": "root________"}';
|
||||
|
||||
final count = await utils.importFromJSON(noChildrenJson);
|
||||
|
||||
expect(count, equals(0));
|
||||
});
|
||||
|
||||
test('should filter out tags folder during import', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'tags________',
|
||||
'root': 'tagsFolder',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [],
|
||||
},
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'root': 'bookmarksMenuFolder',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
|
||||
final count = await utils.importFromJSON(
|
||||
jsonEncode(jsonData),
|
||||
replace: true,
|
||||
);
|
||||
|
||||
// Only the menu folder should be processed, tags should be filtered
|
||||
expect(count, equals(0)); // No bookmarks, just folders
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
|
||||
test('should erase everything when replace is true', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
|
||||
await utils.importFromJSON(jsonEncode(jsonData), replace: true);
|
||||
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
|
||||
test('should not erase when replace is false', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
verifyNever(mockService.eraseEverything(any));
|
||||
});
|
||||
|
||||
test('should import bookmarks with URI field', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'bookmark1___',
|
||||
'title': 'Test Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark1___');
|
||||
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
expect(count, equals(1));
|
||||
verify(
|
||||
mockService.addItem(
|
||||
'menu________',
|
||||
Uri.parse('https://example.com'),
|
||||
'Test Bookmark',
|
||||
0,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should import bookmarks with URL field', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'bookmark1___',
|
||||
'title': 'Test Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'url': 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark1___');
|
||||
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
expect(count, equals(1));
|
||||
verify(
|
||||
mockService.addItem(
|
||||
'menu________',
|
||||
Uri.parse('https://example.com'),
|
||||
'Test Bookmark',
|
||||
0,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should skip bookmarks with invalid URLs', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'invalid1____',
|
||||
'title': 'Invalid URL',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'not a valid url',
|
||||
},
|
||||
{
|
||||
'guid': 'valid1______',
|
||||
'title': 'Valid URL',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'valid1______');
|
||||
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
// Only one valid bookmark should be imported
|
||||
expect(count, equals(1));
|
||||
// Note: position is 1 because the invalid bookmark was skipped first
|
||||
verify(
|
||||
mockService.addItem(
|
||||
'menu________',
|
||||
Uri.parse('https://example.com'),
|
||||
'Valid URL',
|
||||
1,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should import nested folders recursively', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'folder1_____',
|
||||
'title': 'Folder 1',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'bookmark1___',
|
||||
'title': 'Nested Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder1_____');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark1___');
|
||||
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
expect(count, equals(1));
|
||||
verify(mockService.addFolder('menu________', 'Folder 1', 0)).called(1);
|
||||
verify(
|
||||
mockService.addItem(
|
||||
'folder1_____',
|
||||
Uri.parse('https://example.com'),
|
||||
'Nested Bookmark',
|
||||
0,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('should handle separators gracefully (skip them)', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'bookmark1___',
|
||||
'title': 'First Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com/1',
|
||||
},
|
||||
{'guid': 'separator___', 'type': 'text/x-moz-place-separator'},
|
||||
{
|
||||
'guid': 'bookmark2___',
|
||||
'title': 'Second Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com/2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((invocation) async => 'generated_guid');
|
||||
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
// Two bookmarks, separator should be skipped
|
||||
expect(count, equals(2));
|
||||
verify(mockService.addItem(any, any, any, any)).called(2);
|
||||
});
|
||||
|
||||
test('should fixup place: queries with folder shortcuts', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'unfiled_____',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'id': '5',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'folder1_____',
|
||||
'title': 'Test Folder',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'id': '6',
|
||||
'children': [],
|
||||
},
|
||||
{
|
||||
'guid': 'shortcut1___',
|
||||
'title': 'Folder Shortcut',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'place:folder=6',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder1_____');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'shortcut1___');
|
||||
|
||||
await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
// Capture the URI argument to verify it was fixed up
|
||||
// Note: position is 1 because the folder was added first at position 0
|
||||
final captured = verify(
|
||||
mockService.addItem('unfiled_____', captureAny, 'Folder Shortcut', 1),
|
||||
).captured;
|
||||
|
||||
expect((captured[0] as Uri).toString(), contains('parent=folder1_____'));
|
||||
});
|
||||
|
||||
test('should handle invalid folder references in place: queries', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'unfiled_____',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'shortcut1___',
|
||||
'title': 'Invalid Folder Shortcut',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'place:folder=999999',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'shortcut1___');
|
||||
|
||||
await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
final captured = verify(
|
||||
mockService.addItem(
|
||||
'unfiled_____',
|
||||
captureAny,
|
||||
'Invalid Folder Shortcut',
|
||||
0,
|
||||
),
|
||||
).captured;
|
||||
|
||||
final url = (captured[0] as Uri).toString();
|
||||
expect(url, contains('invalidOldParentId=999999'));
|
||||
expect(url, contains('excludeItems=1'));
|
||||
});
|
||||
|
||||
test('should count imported bookmarks correctly from fixture', () async {
|
||||
// Load the fixture
|
||||
final fixtureFile = File('test/utils/bookmarks/fixtures/bookmarks.json');
|
||||
final jsonString = await fixtureFile.readAsString();
|
||||
|
||||
// Mock the service calls
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((invocation) async => 'generated_guid');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((invocation) async => 'generated_guid');
|
||||
|
||||
final count = await utils.importFromJSON(jsonString, replace: true);
|
||||
|
||||
// The fixture has several bookmarks - we should count only valid ones
|
||||
expect(count, greaterThan(0));
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
|
||||
test('should handle import errors gracefully', () async {
|
||||
final jsonData = {
|
||||
'children': [
|
||||
{
|
||||
'guid': 'menu________',
|
||||
'type': 'text/x-moz-place-container',
|
||||
'children': [
|
||||
{
|
||||
'guid': 'bookmark1___',
|
||||
'title': 'Test Bookmark',
|
||||
'type': 'text/x-moz-place',
|
||||
'uri': 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenThrow(Exception('Database error'));
|
||||
|
||||
// Should not throw, but should log and continue
|
||||
final count = await utils.importFromJSON(jsonEncode(jsonData));
|
||||
|
||||
expect(count, equals(0)); // Failed to add
|
||||
});
|
||||
});
|
||||
|
||||
group('BookmarkJSONUtils - Export', () {
|
||||
test('should export bookmark tree to JSON', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!['guid'], equals('menu________'));
|
||||
expect(result['title'], equals('Bookmarks Menu'));
|
||||
expect(result['type'], equals('text/x-moz-place-container'));
|
||||
expect(result['root'], equals('bookmarksMenuFolder'));
|
||||
expect(result['children'], isA<List>());
|
||||
expect((result['children'] as List).length, equals(1));
|
||||
|
||||
final child = (result['children'] as List)[0] as Map<String, dynamic>;
|
||||
expect(child['guid'], equals('bookmark1___'));
|
||||
expect(child['title'], equals('Test Bookmark'));
|
||||
expect(child['url'], equals('https://example.com'));
|
||||
expect(child['type'], equals('text/x-moz-place'));
|
||||
});
|
||||
|
||||
test('should skip bookmarks with invalid URLs during export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'invalid1____',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'Invalid Bookmark',
|
||||
url: '', // Empty URL
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'valid1______',
|
||||
parentGuid: 'menu________',
|
||||
position: 1,
|
||||
title: 'Valid Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
|
||||
expect(result, isNotNull);
|
||||
final children = result!['children'] as List;
|
||||
// Only the valid bookmark should be exported
|
||||
expect(children.length, equals(1));
|
||||
expect(children[0]['guid'], equals('valid1______'));
|
||||
});
|
||||
|
||||
test('should handle separators in export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'separator___',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'should be ignored',
|
||||
url: null,
|
||||
dateAdded: 1361551979380988,
|
||||
lastModified: 1361551979380988,
|
||||
type: BookmarkNodeType.separator,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
|
||||
expect(result, isNotNull);
|
||||
final children = result!['children'] as List;
|
||||
expect(children.length, equals(1));
|
||||
|
||||
final separator = children[0];
|
||||
expect(separator['type'], equals('text/x-moz-place-separator'));
|
||||
expect(separator['title'], equals('')); // Title should be empty
|
||||
});
|
||||
|
||||
test('should assign correct root names', () async {
|
||||
final testCases = [
|
||||
(BookmarkRoot.menu, 'bookmarksMenuFolder'),
|
||||
(BookmarkRoot.toolbar, 'toolbarFolder'),
|
||||
(BookmarkRoot.unfiled, 'unfiledBookmarksFolder'),
|
||||
(BookmarkRoot.mobile, 'mobileFolder'),
|
||||
];
|
||||
|
||||
for (final testCase in testCases) {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: testCase.$1.id,
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Test Root',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(testCase.$1.id, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: testCase.$1);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!['root'], equals(testCase.$2));
|
||||
}
|
||||
});
|
||||
|
||||
test('should preserve correct index values for children', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'First',
|
||||
url: 'https://example.com/1',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'bookmark2___',
|
||||
parentGuid: 'menu________',
|
||||
position: 1,
|
||||
title: 'Second',
|
||||
url: 'https://example.com/2',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
BookmarkNode(
|
||||
guid: 'bookmark3___',
|
||||
parentGuid: 'menu________',
|
||||
position: 2,
|
||||
title: 'Third',
|
||||
url: 'https://example.com/3',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
|
||||
expect(result, isNotNull);
|
||||
final children = result!['children'] as List;
|
||||
expect(children.length, equals(3));
|
||||
expect(children[0]['index'], equals(0));
|
||||
expect(children[1]['index'], equals(1));
|
||||
expect(children[2]['index'], equals(2));
|
||||
});
|
||||
|
||||
test('should throw when tree cannot be fetched', () {
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => null);
|
||||
|
||||
expect(
|
||||
() => utils.exportToJson(root: BookmarkRoot.menu),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('should include typeCode in export', () async {
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
final result = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!['typeCode'], equals(BookmarkNodeType.folder.index + 1));
|
||||
|
||||
final child = (result['children'] as List)[0] as Map<String, dynamic>;
|
||||
expect(child['typeCode'], equals(BookmarkNodeType.item.index + 1));
|
||||
});
|
||||
});
|
||||
|
||||
group('BookmarkJSONUtils - Import/Export Round-Trip', () {
|
||||
test('should preserve data through export and re-import', () async {
|
||||
// Setup initial data
|
||||
final mockNode = BookmarkNode(
|
||||
guid: 'menu________',
|
||||
parentGuid: 'root________',
|
||||
position: 0,
|
||||
title: 'Bookmarks Menu',
|
||||
url: null,
|
||||
dateAdded: 1361551978957783,
|
||||
lastModified: 1361551979382837,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'folder1_____',
|
||||
parentGuid: 'menu________',
|
||||
position: 0,
|
||||
title: 'Test Folder',
|
||||
url: null,
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.folder,
|
||||
children: [
|
||||
BookmarkNode(
|
||||
guid: 'bookmark1___',
|
||||
parentGuid: 'folder1_____',
|
||||
position: 0,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
dateAdded: 1361551979350273,
|
||||
lastModified: 1361551979376699,
|
||||
type: BookmarkNodeType.item,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
when(
|
||||
mockService.getTree(any, recursive: true),
|
||||
).thenAnswer((_) async => mockNode);
|
||||
|
||||
// Export
|
||||
final exported = await utils.exportToJson(root: BookmarkRoot.menu);
|
||||
expect(exported, isNotNull);
|
||||
|
||||
// Re-import
|
||||
when(mockService.eraseEverything(any)).thenAnswer((_) async {});
|
||||
when(
|
||||
mockService.addFolder(any, any, any),
|
||||
).thenAnswer((_) async => 'folder1_____');
|
||||
when(
|
||||
mockService.addItem(any, any, any, any),
|
||||
).thenAnswer((_) async => 'bookmark1___');
|
||||
|
||||
final jsonString = jsonEncode({
|
||||
'children': [exported],
|
||||
});
|
||||
final count = await utils.importFromJSON(jsonString, replace: true);
|
||||
|
||||
expect(count, equals(1)); // One bookmark imported
|
||||
verify(mockService.eraseEverything(BookmarkRoot.root)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in weblibre/test/features/geckoview/features/bookmarks/utils/bookmark_json_utils_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i3;
|
||||
|
||||
import 'package:flutter_mozilla_components/src/domain/services/gecko_bookmarks.dart'
|
||||
as _i2;
|
||||
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart' as _i4;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i5;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
/// A class which mocks [GeckoBookmarksService].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGeckoBookmarksService extends _i1.Mock
|
||||
implements _i2.GeckoBookmarksService {
|
||||
MockGeckoBookmarksService() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i3.Future<_i4.BookmarkNode?> getTree(
|
||||
String? guid, {
|
||||
bool? recursive = false,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getTree, [guid], {#recursive: recursive}),
|
||||
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
|
||||
)
|
||||
as _i3.Future<_i4.BookmarkNode?>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i4.BookmarkNode?> getBookmark(String? guid) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getBookmark, [guid]),
|
||||
returnValue: _i3.Future<_i4.BookmarkNode?>.value(),
|
||||
)
|
||||
as _i3.Future<_i4.BookmarkNode?>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> getBookmarksWithUrl(Uri? url) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getBookmarksWithUrl, [url]),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> getRecentBookmarks(
|
||||
int? limit, {
|
||||
Duration? maxAge = Duration.zero,
|
||||
DateTime? currentTime,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getRecentBookmarks,
|
||||
[limit],
|
||||
{#maxAge: maxAge, #currentTime: currentTime},
|
||||
),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<List<_i4.BookmarkNode>> searchBookmarks(
|
||||
String? query, {
|
||||
int? limit = 10,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#searchBookmarks, [query], {#limit: limit}),
|
||||
returnValue: _i3.Future<List<_i4.BookmarkNode>>.value(
|
||||
<_i4.BookmarkNode>[],
|
||||
),
|
||||
)
|
||||
as _i3.Future<List<_i4.BookmarkNode>>);
|
||||
|
||||
@override
|
||||
_i3.Future<String> addItem(
|
||||
String? parentGuid,
|
||||
Uri? url,
|
||||
String? title,
|
||||
int? position,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addItem, [parentGuid, url, title, position]),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#addItem, [parentGuid, url, title, position]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<String> addFolder(
|
||||
String? parentGuid,
|
||||
String? title,
|
||||
int? position,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addFolder, [parentGuid, title, position]),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#addFolder, [parentGuid, title, position]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<void> updateNode(String? guid, _i4.BookmarkInfo? info) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateNode, [guid, info]),
|
||||
returnValue: _i3.Future<void>.value(),
|
||||
returnValueForMissingStub: _i3.Future<void>.value(),
|
||||
)
|
||||
as _i3.Future<void>);
|
||||
|
||||
@override
|
||||
_i3.Future<bool> deleteNode(String? guid) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteNode, [guid]),
|
||||
returnValue: _i3.Future<bool>.value(false),
|
||||
)
|
||||
as _i3.Future<bool>);
|
||||
|
||||
@override
|
||||
_i3.Future<void> eraseEverything(_i2.BookmarkRoot? root) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#eraseEverything, [root]),
|
||||
returnValue: _i3.Future<void>.value(),
|
||||
returnValueForMissingStub: _i3.Future<void>.value(),
|
||||
)
|
||||
as _i3.Future<void>);
|
||||
}
|
||||
+1171
File diff suppressed because it is too large
Load Diff
+355
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart';
|
||||
|
||||
void main() {
|
||||
late ProviderContainer container;
|
||||
late UrlUnshortenerService service;
|
||||
|
||||
setUp(() {
|
||||
container = ProviderContainer();
|
||||
service = container.read(urlUnshortenerServiceProvider.notifier);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
container.dispose();
|
||||
});
|
||||
|
||||
group('warning list asset compatibility', () {
|
||||
test('parses current MISP url-shortener list format', () {
|
||||
final rawJson = File(
|
||||
'assets/preferences/url-shortener-list.json',
|
||||
).readAsStringSync();
|
||||
|
||||
final decoded = jsonDecode(rawJson) as Map<String, dynamic>;
|
||||
expect(decoded['type'], 'hostname');
|
||||
expect(decoded['matching_attributes'], isA<List<dynamic>>());
|
||||
expect(decoded['list'], isA<List<dynamic>>());
|
||||
|
||||
final hosts = service.parseSupportedShortenerHosts(rawJson);
|
||||
expect(hosts.length, greaterThan(200));
|
||||
expect(hosts, contains('bit.ly'));
|
||||
expect(service.isSupportedShortenerHost('www.bit.ly', hosts), isTrue);
|
||||
expect(
|
||||
service.isSupportedShortenerUrl('https://example.com', hosts),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('supported shortener host checks', () {
|
||||
test('parses list hosts from warning list json', () {
|
||||
final hosts = service.parseSupportedShortenerHosts(
|
||||
jsonEncode({
|
||||
'list': ['bit.ly', 't.co', 'TinyURL.com', '*.short.cm'],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hosts, containsAll({'bit.ly', 't.co', 'tinyurl.com', 'short.cm'}));
|
||||
});
|
||||
|
||||
test('normalizes URL-like entries to hostnames', () {
|
||||
final hosts = service.parseSupportedShortenerHosts(
|
||||
jsonEncode({
|
||||
'list': ['https://bit.ly/abc', 'tinyurl.com/path?a=1', 't.co/#frag'],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hosts, containsAll({'bit.ly', 'tinyurl.com', 't.co'}));
|
||||
});
|
||||
|
||||
test('matches exact and subdomain hosts', () {
|
||||
const supportedHosts = {'bit.ly', 't.co'};
|
||||
|
||||
expect(
|
||||
service.isSupportedShortenerUrl('https://bit.ly/abc', supportedHosts),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
service.isSupportedShortenerUrl('https://www.t.co/abc', supportedHosts),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
service.isSupportedShortenerUrl(
|
||||
'https://example.com/abc',
|
||||
supportedHosts,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('matches urls without a scheme', () {
|
||||
const supportedHosts = {'tinyurl.com'};
|
||||
|
||||
expect(
|
||||
service.isSupportedShortenerUrl('tinyurl.com/abc', supportedHosts),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
service.isSupportedShortenerUrl('notinyurl.com/abc', supportedHosts),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('unshortenUrl', () {
|
||||
group('unauthenticated requests', () {
|
||||
test('resolves shortened URL successfully', () async {
|
||||
final client = MockClient((request) async {
|
||||
expect(request.url.host, 'unshorten.me');
|
||||
expect(request.url.pathSegments, contains('json'));
|
||||
expect(request.headers, isNot(contains('Authorization')));
|
||||
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'success': true,
|
||||
'resolved_url': 'https://example.com/full-article',
|
||||
'remaining_calls': 8,
|
||||
'usage_count': 10,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc123',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.finalUrl, 'https://example.com/full-article');
|
||||
expect(result.remainingCalls, 8);
|
||||
expect(result.usageCount, 10);
|
||||
expect(result.error, isNull);
|
||||
});
|
||||
|
||||
test('encodes URL in request path', () async {
|
||||
late Uri capturedUri;
|
||||
final client = MockClient((request) async {
|
||||
capturedUri = request.url;
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'success': true,
|
||||
'resolved_url': 'https://example.com',
|
||||
}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.unshortenUrl(
|
||||
'https://bit.ly/test?a=1&b=2',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(
|
||||
capturedUri.toString(),
|
||||
contains(Uri.encodeComponent('https://bit.ly/test?a=1&b=2')),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns error on API failure', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({'success': false, 'error': 'Could not resolve URL'}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://invalid-short.url/x',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.error, 'Could not resolve URL');
|
||||
expect(result.finalUrl, isNull);
|
||||
});
|
||||
|
||||
test('returns error on HTTP error status', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response('Server Error', 500);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.error, 'HTTP 500');
|
||||
});
|
||||
|
||||
test('returns error on rate limit', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response('Too Many Requests', 429);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.error, 'HTTP 429');
|
||||
});
|
||||
|
||||
test('handles missing success field', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({'resolved_url': 'https://example.com'}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
client: client,
|
||||
);
|
||||
|
||||
// success defaults to false when missing
|
||||
expect(result.success, isFalse);
|
||||
});
|
||||
|
||||
test('handles missing error field on failure', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(jsonEncode({'success': false}), 200);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.error, 'Unknown error');
|
||||
});
|
||||
});
|
||||
|
||||
group('authenticated requests', () {
|
||||
test('sends token in Authorization header', () async {
|
||||
late Map<String, String> capturedHeaders;
|
||||
final client = MockClient((request) async {
|
||||
capturedHeaders = request.headers;
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'unshortened_url': 'https://example.com/page',
|
||||
'remaining_calls': 95,
|
||||
'usage_count': 100,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
token: 'my-api-token',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(capturedHeaders['Authorization'], 'Token my-api-token');
|
||||
});
|
||||
|
||||
test('uses v2 API endpoint with token', () async {
|
||||
late Uri capturedUri;
|
||||
final client = MockClient((request) async {
|
||||
capturedUri = request.url;
|
||||
return http.Response(
|
||||
jsonEncode({'unshortened_url': 'https://example.com'}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
token: 'token123',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(capturedUri.pathSegments, contains('v2'));
|
||||
expect(capturedUri.pathSegments, contains('unshorten'));
|
||||
expect(capturedUri.queryParameters['url'], isNotNull);
|
||||
});
|
||||
|
||||
test('resolves URL with token successfully', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'unshortened_url': 'https://example.com/target',
|
||||
'remaining_calls': 50,
|
||||
'usage_count': 100,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://t.co/abc',
|
||||
token: 'valid-token',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.finalUrl, 'https://example.com/target');
|
||||
expect(result.remainingCalls, 50);
|
||||
expect(result.usageCount, 100);
|
||||
});
|
||||
|
||||
test('returns error from authenticated API', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(jsonEncode({'error': 'Invalid token'}), 200);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
token: 'bad-token',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.error, 'Invalid token');
|
||||
});
|
||||
|
||||
test('handles empty error field as success', () async {
|
||||
final client = MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({'error': '', 'unshortened_url': 'https://example.com'}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.unshortenUrl(
|
||||
'https://bit.ly/abc',
|
||||
token: 'token',
|
||||
client: client,
|
||||
);
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.finalUrl, 'https://example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
|
||||
void main() {
|
||||
group('TabMode value semantics', () {
|
||||
test('isolated modes with same context are equal and hash equally', () {
|
||||
final first = TabMode.isolated('iso1_same');
|
||||
final second = TabMode.isolated('iso1_same');
|
||||
|
||||
expect(first, equals(second));
|
||||
expect(first.hashCode, equals(second.hashCode));
|
||||
});
|
||||
|
||||
test('isolated modes with different contexts are not equal', () {
|
||||
final first = TabMode.isolated('iso1_a');
|
||||
final second = TabMode.isolated('iso1_b');
|
||||
|
||||
expect(first, isNot(equals(second)));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
|
||||
|
||||
void main() {
|
||||
group('SharedContent.parse', () {
|
||||
test('returns SharedUrl for explicit https uri', () {
|
||||
final parsed = SharedContent.parse('https://weblibre.eu/path');
|
||||
|
||||
expect(parsed, isA<SharedUrl>());
|
||||
expect((parsed as SharedUrl).url.host, 'weblibre.eu');
|
||||
});
|
||||
|
||||
test('returns SharedText for explicit non-http scheme', () {
|
||||
final parsed = SharedContent.parse('moz-extension://abc/index.html');
|
||||
|
||||
expect(parsed, isA<SharedText>());
|
||||
});
|
||||
|
||||
test('returns SharedText for plain sentence', () {
|
||||
final parsed = SharedContent.parse('WebLibre README.md');
|
||||
|
||||
expect(parsed, isA<SharedText>());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:weblibre/presentation/main_app.dart';
|
||||
|
||||
void main() {
|
||||
group('applyAppMediaQueryOverrides', () {
|
||||
test('returns the original media query when no overrides are enabled', () {
|
||||
const mediaQuery = MediaQueryData();
|
||||
|
||||
final result = applyAppMediaQueryOverrides(
|
||||
mediaQuery: mediaQuery,
|
||||
uiScaleFactor: 1.0,
|
||||
disableAnimations: false,
|
||||
);
|
||||
|
||||
expect(result, same(mediaQuery));
|
||||
});
|
||||
|
||||
test(
|
||||
'preserves the system animation preference when app override is off',
|
||||
() {
|
||||
const mediaQuery = MediaQueryData(disableAnimations: true);
|
||||
|
||||
final result = applyAppMediaQueryOverrides(
|
||||
mediaQuery: mediaQuery,
|
||||
uiScaleFactor: 1.0,
|
||||
disableAnimations: false,
|
||||
);
|
||||
|
||||
expect(result.disableAnimations, isTrue);
|
||||
expect(result, same(mediaQuery));
|
||||
},
|
||||
);
|
||||
|
||||
test('forces animations off when the app setting is enabled', () {
|
||||
// ignore: avoid_redundant_argument_values
|
||||
const mediaQuery = MediaQueryData(disableAnimations: false);
|
||||
|
||||
final result = applyAppMediaQueryOverrides(
|
||||
mediaQuery: mediaQuery,
|
||||
uiScaleFactor: 1.0,
|
||||
disableAnimations: true,
|
||||
);
|
||||
|
||||
expect(result.disableAnimations, isTrue);
|
||||
});
|
||||
|
||||
test('applies ui scale on top of the base text scaler', () {
|
||||
const mediaQuery = MediaQueryData();
|
||||
|
||||
final result = applyAppMediaQueryOverrides(
|
||||
mediaQuery: mediaQuery,
|
||||
uiScaleFactor: 1.25,
|
||||
disableAnimations: false,
|
||||
);
|
||||
|
||||
expect(result.textScaler.scale(20), 25);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
|
||||
|
||||
<DL><p>
|
||||
<DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
|
||||
<DT><A HREF="b0rked" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
|
||||
</DL><p>
|
||||
<DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
|
||||
<DD>folder test comment
|
||||
<DL><p>
|
||||
<DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
|
||||
<DD>item description
|
||||
</DL>
|
||||
<DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://example.tld">Example.tld</A>
|
||||
</DL><p>
|
||||
<DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
|
||||
<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
|
||||
<DL><p>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
|
||||
<DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
|
||||
<DT><A HREF="http://bogus-icon.mozilla.com/" ICON="b0rked" ID="rdf:#$GvPhC3">Getting Started</A>
|
||||
<DD>Livemark test comment
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"id": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551978957783,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"id": 2,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551979382837,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FF",
|
||||
"title": "Mozilla Firefox",
|
||||
"id": 6,
|
||||
"parent": 2,
|
||||
"dateAdded": 1361551979350273,
|
||||
"lastModified": 1361551979376699,
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FG",
|
||||
"title": "Help and Tutorials",
|
||||
"id": 7,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979356436,
|
||||
"lastModified": 1361551979362718,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/help/",
|
||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FH",
|
||||
"index": 1,
|
||||
"title": "Customize Firefox",
|
||||
"id": 8,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979365662,
|
||||
"lastModified": 1361551979368077,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
|
||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FJ",
|
||||
"index": 3,
|
||||
"title": "About Us",
|
||||
"id": 10,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979376699,
|
||||
"lastModified": 1361551979379060,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/about/",
|
||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FI",
|
||||
"index": 2,
|
||||
"title": "Get Involved",
|
||||
"id": 9,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979371071,
|
||||
"lastModified": 1361551979373745,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
|
||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "QFM-QnE2ZpMz",
|
||||
"title": "Test null postData",
|
||||
"index": 4,
|
||||
"dateAdded": 1481639510868000,
|
||||
"lastModified": 1489563704300000,
|
||||
"id": 17,
|
||||
"charset": "UTF-8",
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": "The best"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.com/search?q=%s&suggid=",
|
||||
"postData": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FK",
|
||||
"index": 1,
|
||||
"title": "",
|
||||
"id": 11,
|
||||
"parent": 2,
|
||||
"dateAdded": 1361551979380988,
|
||||
"lastModified": 1361551979380988,
|
||||
"type": "text/x-moz-place-separator"
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FL",
|
||||
"index": 2,
|
||||
"title": "test",
|
||||
"id": 12,
|
||||
"parent": 2,
|
||||
"dateAdded": 1177541020000000,
|
||||
"lastModified": 1177541050000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "folder test comment"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9GX",
|
||||
"title": "test post keyword",
|
||||
"id": 13,
|
||||
"parent": 12,
|
||||
"dateAdded": 1177375336000000,
|
||||
"lastModified": 1177375423000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "item description"
|
||||
},
|
||||
{
|
||||
"name": "bookmarkProperties/loadInSidebar",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 1,
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://test/post",
|
||||
"keyword": "test",
|
||||
"charset": "ISO-8859-1",
|
||||
"postData": "hidden1%3Dbar&text1%3D%25s"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"title": "Bookmarks Toolbar",
|
||||
"id": 3,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1177541050000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FB",
|
||||
"title": "Getting Started",
|
||||
"id": 15,
|
||||
"parent": 3,
|
||||
"dateAdded": 1361551979409695,
|
||||
"lastModified": 1361551979412080,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/central/",
|
||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FR",
|
||||
"index": 1,
|
||||
"title": "Latest Headlines",
|
||||
"id": 16,
|
||||
"parent": 3,
|
||||
"dateAdded": 1361551979451584,
|
||||
"lastModified": 1361551979457086,
|
||||
"livemark": 1,
|
||||
"annos": [
|
||||
{
|
||||
"name": "livemark/feedURI",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
|
||||
},
|
||||
{
|
||||
"name": "livemark/siteURI",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"title": "Tags",
|
||||
"id": 4,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551978957783,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "tagsFolder",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"title": "Unsorted Bookmarks",
|
||||
"id": 5,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1177541050000000,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FW",
|
||||
"title": "Example.tld",
|
||||
"id": 14,
|
||||
"parent": 5,
|
||||
"dateAdded": 1361551979401846,
|
||||
"lastModified": 1361551979402952,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.tld/"
|
||||
},
|
||||
{
|
||||
"guid": "Cfkety492Afk",
|
||||
"title": "test tagged bookmark",
|
||||
"id": 15,
|
||||
"parent": 5,
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.tld/tagged",
|
||||
"tags": "foo"
|
||||
},
|
||||
{
|
||||
"guid": "lOZGoFR1eXbl",
|
||||
"title": "Bookmarks Toolbar Shortcut",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 16,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=TOOLBAR"
|
||||
},
|
||||
{
|
||||
"guid": "7yJWnBVhjRtP",
|
||||
"title": "Folder Shortcut",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 17,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6"
|
||||
},
|
||||
{
|
||||
"guid": "vm5QXWuWc12l",
|
||||
"title": "Folder Shortcut 2",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 18,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6123443"
|
||||
},
|
||||
{
|
||||
"guid": "Icg1XlIozA1D",
|
||||
"title": "Folder Shortcut 3",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 18,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6&folder=BOOKMARKS_MENU"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
|
||||
|
||||
<DL><p>
|
||||
<DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
|
||||
</DL><p>
|
||||
<HR>
|
||||
<DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
|
||||
<DD>folder test comment
|
||||
<DL><p>
|
||||
<DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
|
||||
<DD>item description
|
||||
</DL>
|
||||
<DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://example.tld">Example.tld</A>
|
||||
</DL><p>
|
||||
<DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
|
||||
<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
|
||||
<DL><p>
|
||||
<DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
|
||||
<DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
|
||||
<DT><A LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines No Site</A>
|
||||
<DD>Livemark test comment
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"id": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551978957783,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"id": 2,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551979382837,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FG",
|
||||
"title": "Help and Tutorials",
|
||||
"id": 7,
|
||||
"dateAdded": 1361551979356436,
|
||||
"lastModified": 1361551979362718,
|
||||
"type": "x/invalid",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/help/"
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FH",
|
||||
"index": 1,
|
||||
"title": "Customize Firefox",
|
||||
"id": 8,
|
||||
"dateAdded": 1361551979365662,
|
||||
"lastModified": 1361551979368077,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/"
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FG",
|
||||
"title": "Bad URL",
|
||||
"id": 9,
|
||||
"dateAdded": 1361551979356436,
|
||||
"lastModified": 1361551979362718,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http:///"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"id": 2,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551979382837,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FG",
|
||||
"title": "Bad URL",
|
||||
"id": 9,
|
||||
"dateAdded": 1361551979356436,
|
||||
"lastModified": 1361551979362718,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http:///"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<LINK REL="localization" HREF="bookmarks_html_localized.ftl">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
|
||||
|
||||
<DL><p>
|
||||
<DT><H3 ID="rdf:#$ZvPhC3" data-l10n-id="bookmarks-html-localized-folder">bookmarks-html-localized-folder</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://www.mozilla.com/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1" data-l10n-id="bookmarks-html-localized-bookmark">bookmarks-html-localized-bookmark</A>
|
||||
</DL><p>
|
||||
</DL><p>
|
||||
</BODY>
|
||||
</HTML>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<HTML>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<Title>Bookmarks</Title>
|
||||
<H1>Bookmarks</H1>
|
||||
<DT><H3>Subtitle</H3>
|
||||
<DL><p>
|
||||
<DT><A HREF="http://www.mozilla.org/">Mozilla</A>
|
||||
</DL><p>
|
||||
</HTML>
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"id": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551978957783,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"id": 2,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551979382837,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FF",
|
||||
"title": "Mozilla Firefox",
|
||||
"id": 6,
|
||||
"parent": 2,
|
||||
"dateAdded": 1361551979350273,
|
||||
"lastModified": 1361551979376699,
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FG",
|
||||
"title": "Help and Tutorials",
|
||||
"id": 7,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979356436,
|
||||
"lastModified": 1361551979362718,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/help/",
|
||||
"iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FH",
|
||||
"index": 1,
|
||||
"title": "Customize Firefox",
|
||||
"id": 8,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979365662,
|
||||
"lastModified": 1361551979368077,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
|
||||
"iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FJ",
|
||||
"index": 3,
|
||||
"title": "About Us",
|
||||
"id": 10,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979376699,
|
||||
"lastModified": 1361551979379060,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/about/",
|
||||
"iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FI",
|
||||
"index": 2,
|
||||
"title": "Get Involved",
|
||||
"id": 9,
|
||||
"parent": 6,
|
||||
"dateAdded": 1361551979371071,
|
||||
"lastModified": 1361551979373745,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
|
||||
"iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "QFM-QnE2ZpMz",
|
||||
"title": "Test null postData",
|
||||
"index": 4,
|
||||
"dateAdded": 1481639510868000,
|
||||
"lastModified": 1489563704300000,
|
||||
"id": 17,
|
||||
"charset": "UTF-8",
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": "The best"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.com/search?q=%s&suggid=",
|
||||
"postData": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FK",
|
||||
"index": 1,
|
||||
"title": "",
|
||||
"id": 11,
|
||||
"parent": 2,
|
||||
"dateAdded": 1361551979380988,
|
||||
"lastModified": 1361551979380988,
|
||||
"type": "text/x-moz-place-separator"
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FL",
|
||||
"index": 2,
|
||||
"title": "test",
|
||||
"id": 12,
|
||||
"parent": 2,
|
||||
"dateAdded": 1177541020000000,
|
||||
"lastModified": 1177541050000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "folder test comment"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9GX",
|
||||
"title": "test post keyword",
|
||||
"id": 13,
|
||||
"parent": 12,
|
||||
"dateAdded": 1177375336000000,
|
||||
"lastModified": 1177375423000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "item description"
|
||||
},
|
||||
{
|
||||
"name": "bookmarkProperties/loadInSidebar",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 1,
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://test/post",
|
||||
"keyword": "test",
|
||||
"charset": "ISO-8859-1",
|
||||
"postData": "hidden1%3Dbar&text1%3D%25s"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"title": "Bookmarks Toolbar",
|
||||
"id": 3,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1177541050000000,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FB",
|
||||
"title": "Getting Started",
|
||||
"id": 15,
|
||||
"parent": 3,
|
||||
"dateAdded": 1361551979409695,
|
||||
"lastModified": 1361551979412080,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://en-us.www.mozilla.com/en-US/firefox/central/",
|
||||
"iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
|
||||
},
|
||||
{
|
||||
"guid": "OCyeUO5uu9FR",
|
||||
"index": 1,
|
||||
"title": "Latest Headlines",
|
||||
"id": 16,
|
||||
"parent": 3,
|
||||
"dateAdded": 1361551979451584,
|
||||
"lastModified": 1361551979457086,
|
||||
"livemark": 1,
|
||||
"annos": [
|
||||
{
|
||||
"name": "livemark/feedURI",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
|
||||
},
|
||||
{
|
||||
"name": "livemark/siteURI",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"title": "Tags",
|
||||
"id": 4,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1361551978957783,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "tagsFolder",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"title": "Unsorted Bookmarks",
|
||||
"id": 5,
|
||||
"parent": 1,
|
||||
"dateAdded": 1361551978957783,
|
||||
"lastModified": 1177541050000000,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "OCyeUO5uu9FW",
|
||||
"title": "Example.tld",
|
||||
"id": 14,
|
||||
"parent": 5,
|
||||
"dateAdded": 1361551979401846,
|
||||
"lastModified": 1361551979402952,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.tld/"
|
||||
},
|
||||
{
|
||||
"guid": "Cfkety492Afk",
|
||||
"title": "test tagged bookmark",
|
||||
"id": 15,
|
||||
"parent": 5,
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://example.tld/tagged",
|
||||
"tags": "foo"
|
||||
},
|
||||
{
|
||||
"guid": "lOZGoFR1eXbl",
|
||||
"title": "Bookmarks Toolbar Shortcut",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 16,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=TOOLBAR"
|
||||
},
|
||||
{
|
||||
"guid": "7yJWnBVhjRtP",
|
||||
"title": "Folder Shortcut",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 17,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6"
|
||||
},
|
||||
{
|
||||
"guid": "vm5QXWuWc12l",
|
||||
"title": "Folder Shortcut 2",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 18,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6123443"
|
||||
},
|
||||
{
|
||||
"guid": "Icg1XlIozA1D",
|
||||
"title": "Folder Shortcut 3",
|
||||
"dateAdded": 1507025843703345,
|
||||
"lastModified": 1507025844703124,
|
||||
"id": 18,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=6&folder=BOOKMARKS_MENU"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731479000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 2,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "X6lUyOspVYwi",
|
||||
"title": "Test Pilot",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731768000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 3,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://testpilot.firefox.com/"
|
||||
},
|
||||
{
|
||||
"guid": "XF4yRP6bTuil",
|
||||
"title": "Mobile bookmarks query",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731768000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 11,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=101"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "buy7711R3ZgE",
|
||||
"title": "MDN",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://developer.mozilla.org"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "3qmd_imziEBE",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 5,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 101,
|
||||
"annos": [
|
||||
{
|
||||
"name": "mobile/bookmarksRoot",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "A description of the mobile folder that should be ignored on import"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "_o8e1_zxTJFg",
|
||||
"title": "Get Firefox!",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 7,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://getfirefox.com/"
|
||||
},
|
||||
{
|
||||
"guid": "QCtSqkVYUbXB",
|
||||
"title": "Get Thunderbird!",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731770000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 8,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://getthunderbird.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Other Bookmarks",
|
||||
"index": 3,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 9,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "KIa9iKZab2Z5",
|
||||
"title": "Add-ons",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 10,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://addons.mozilla.org"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731479000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 2,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "Utodo9b0oVws",
|
||||
"title": "Firefox Accounts",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731955000,
|
||||
"lastModified": 1475084731955000,
|
||||
"id": 3,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://accounts.firefox.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder"
|
||||
},
|
||||
{
|
||||
"guid": "3qmd_imziEBE",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 5,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 5,
|
||||
"annos": [
|
||||
{
|
||||
"name": "mobile/bookmarksRoot",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "A description of the mobile folder that should be ignored on import"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "a17yW6-nTxEJ",
|
||||
"title": "Mozilla",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731959000,
|
||||
"lastModified": 1475084731959000,
|
||||
"id": 6,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://mozilla.org/"
|
||||
},
|
||||
{
|
||||
"guid": "xV10h9Wi3FBM",
|
||||
"title": "Bugzilla",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731961000,
|
||||
"lastModified": 1475084731961000,
|
||||
"id": 7,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://bugzilla.mozilla.org/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Other Bookmarks",
|
||||
"index": 3,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 8,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731479000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 2,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "buy7711R3ZgE",
|
||||
"title": "MDN",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 3,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://developer.mozilla.org"
|
||||
},
|
||||
{
|
||||
"guid": "F_LBgd1fS_uQ",
|
||||
"title": "Mobile bookmarks query for first folder",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731768000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 11,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=101"
|
||||
},
|
||||
{
|
||||
"guid": "oIpmQXMWsXvY",
|
||||
"title": "Mobile bookmarks query for second folder",
|
||||
"index": 2,
|
||||
"dateAdded": 1475084731768000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 12,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "place:folder=102"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "3qmd_imziEBE",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 5,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 101,
|
||||
"annos": [
|
||||
{
|
||||
"name": "mobile/bookmarksRoot",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"mimeType": null,
|
||||
"type": 3,
|
||||
"value": "A description of the mobile folder that should be ignored on import"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "a17yW6-nTxEJ",
|
||||
"title": "Mozilla",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731959000,
|
||||
"lastModified": 1475084731959000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://mozilla.org/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 6,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "Utodo9b0oVws",
|
||||
"title": "Firefox Accounts",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731955000,
|
||||
"lastModified": 1475084731955000,
|
||||
"id": 7,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://accounts.firefox.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "o4YjJpgsufU-",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 7,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 102,
|
||||
"annos": [
|
||||
{ "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"children": [
|
||||
{
|
||||
"guid": "sSZ86WT9WbN3",
|
||||
"title": "DXR",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 9,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://dxr.mozilla.org"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Other Bookmarks",
|
||||
"index": 3,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 10,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "xV10h9Wi3FBM",
|
||||
"title": "Bugzilla",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731961000,
|
||||
"lastModified": 1475084731961000,
|
||||
"id": 11,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://bugzilla.mozilla.org/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731479000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 2,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "X6lUyOspVYwi",
|
||||
"title": "Test Pilot",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731768000,
|
||||
"lastModified": 1475084731768000,
|
||||
"id": 3,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://testpilot.firefox.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder"
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Other Bookmarks",
|
||||
"index": 3,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731742000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder"
|
||||
},
|
||||
{
|
||||
"guid": "mobile______",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 4,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 6,
|
||||
"annos": [
|
||||
{ "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "mobileFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "_o8e1_zxTJFg",
|
||||
"title": "Get Firefox!",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731769000,
|
||||
"lastModified": 1475084731769000,
|
||||
"id": 7,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://getfirefox.com/"
|
||||
},
|
||||
{
|
||||
"guid": "QCtSqkVYUbXB",
|
||||
"title": "Get Thunderbird!",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731770000,
|
||||
"lastModified": 1475084731770000,
|
||||
"id": 8,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://getthunderbird.com/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731479000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "menu________",
|
||||
"title": "Bookmarks Menu",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731955000,
|
||||
"id": 2,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "bookmarksMenuFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "Utodo9b0oVws",
|
||||
"title": "Firefox Accounts",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731955000,
|
||||
"lastModified": 1475084731955000,
|
||||
"id": 3,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://accounts.firefox.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Bookmarks Toolbar",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731938000,
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder"
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Other Bookmarks",
|
||||
"index": 3,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731938000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder"
|
||||
},
|
||||
{
|
||||
"guid": "mobile______",
|
||||
"title": "Mobile Bookmarks",
|
||||
"index": 4,
|
||||
"dateAdded": 1475084731479000,
|
||||
"lastModified": 1475084731961000,
|
||||
"id": 6,
|
||||
"annos": [
|
||||
{ "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "mobileFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "a17yW6-nTxEJ",
|
||||
"title": "Mozilla",
|
||||
"index": 0,
|
||||
"dateAdded": 1475084731959000,
|
||||
"lastModified": 1475084731959000,
|
||||
"id": 7,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://mozilla.org/"
|
||||
},
|
||||
{
|
||||
"guid": "xV10h9Wi3FBM",
|
||||
"title": "Bugzilla",
|
||||
"index": 1,
|
||||
"dateAdded": 1475084731961000,
|
||||
"lastModified": 1475084731961000,
|
||||
"id": 8,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://bugzilla.mozilla.org/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/utils/input_classification.dart';
|
||||
|
||||
void main() {
|
||||
group('classifyAddressBarInput', () {
|
||||
test('searches free text containing whitespace', () {
|
||||
final result = classifyAddressBarInput('WebLibre README.md');
|
||||
|
||||
expect(result, isA<SearchInputClassification>());
|
||||
final search = result as SearchInputClassification;
|
||||
expect(search.reason, SearchReason.containsWhitespace);
|
||||
expect(search.query, 'WebLibre README.md');
|
||||
});
|
||||
|
||||
test('navigates schemeless domain', () {
|
||||
final result = classifyAddressBarInput('weblibre.eu');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.schemelessHost);
|
||||
expect(navigation.uri.toString(), 'https://weblibre.eu');
|
||||
});
|
||||
|
||||
test('navigates localhost over http', () {
|
||||
final result = classifyAddressBarInput('localhost:8080');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.localhost);
|
||||
expect(navigation.uri.toString(), 'http://localhost:8080');
|
||||
});
|
||||
|
||||
test('navigates explicit moz-extension uri', () {
|
||||
final result = classifyAddressBarInput('moz-extension://abc/index.html');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.explicitScheme);
|
||||
expect(navigation.uri.scheme, 'moz-extension');
|
||||
});
|
||||
|
||||
test('rejects explicit unsupported scheme as invalid', () {
|
||||
final result = classifyAddressBarInput('myapp://callback?token=abc');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('rejects dotted explicit unsupported scheme as invalid', () {
|
||||
final result = classifyAddressBarInput('my.app://callback');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('rejects javascript scheme as invalid', () {
|
||||
final result = classifyAddressBarInput('JAVASCRIPT:alert(1)');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('does not decode encoded scheme before classification', () {
|
||||
final result = classifyAddressBarInput('%6aavascript:alert(1)');
|
||||
|
||||
expect(result, isA<SearchInputClassification>());
|
||||
});
|
||||
|
||||
test('rejects control chars as invalid', () {
|
||||
final result = classifyAddressBarInput('example.com\x00path');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.containsControlChars);
|
||||
});
|
||||
|
||||
test('invalid schemeless port falls back to search', () {
|
||||
final result = classifyAddressBarInput('weblibre.eu:99999');
|
||||
|
||||
expect(result, isA<SearchInputClassification>());
|
||||
});
|
||||
|
||||
test('explicit disallowed scheme with whitespace remains invalid', () {
|
||||
final result = classifyAddressBarInput('javascript: alert(1)');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('unknown explicit scheme with whitespace remains invalid', () {
|
||||
final result = classifyAddressBarInput('myapp: secret token');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('colon-prefixed query with whitespace is treated as invalid', () {
|
||||
final result = classifyAddressBarInput('site:weblibre.eu privacy');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('symbolic colon query with whitespace is treated as invalid', () {
|
||||
final result = classifyAddressBarInput('c++: tutorial');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('navigates schemeless domain with valid port', () {
|
||||
final result = classifyAddressBarInput('weblibre.eu:8080');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.schemelessHost);
|
||||
expect(navigation.uri.toString(), 'https://weblibre.eu:8080');
|
||||
});
|
||||
|
||||
test('navigates IP literal', () {
|
||||
final result = classifyAddressBarInput('192.168.1.1');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.ipLiteral);
|
||||
expect(navigation.uri.toString(), 'https://192.168.1.1');
|
||||
});
|
||||
|
||||
test('navigates explicit scheme with space in path', () {
|
||||
final result = classifyAddressBarInput('https://example.com/a b');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.explicitScheme);
|
||||
expect(navigation.uri.host, 'example.com');
|
||||
});
|
||||
|
||||
test('rejects data scheme as invalid', () {
|
||||
final result = classifyAddressBarInput(
|
||||
'data:text/html,<script>alert(1)</script>',
|
||||
);
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.unsupportedScheme);
|
||||
});
|
||||
|
||||
test('treats missing scheme prefix as search', () {
|
||||
final result = classifyAddressBarInput('://example.com');
|
||||
|
||||
expect(result, isA<SearchInputClassification>());
|
||||
});
|
||||
|
||||
test('navigates URL with credentials', () {
|
||||
final result = classifyAddressBarInput('https://user:pass@example.com');
|
||||
|
||||
expect(result, isA<NavigateInputClassification>());
|
||||
final navigation = result as NavigateInputClassification;
|
||||
expect(navigation.reason, NavigationReason.explicitScheme);
|
||||
expect(navigation.uri.host, 'example.com');
|
||||
});
|
||||
|
||||
test('rejects very long input as tooLong', () {
|
||||
final longInput = 'a' * 5000;
|
||||
final result = classifyAddressBarInput(longInput);
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.tooLong);
|
||||
});
|
||||
|
||||
test('rejects empty input as emptyInput', () {
|
||||
final result = classifyAddressBarInput('');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.emptyInput);
|
||||
});
|
||||
|
||||
test('rejects whitespace-only input as emptyInput', () {
|
||||
final result = classifyAddressBarInput(' ');
|
||||
|
||||
expect(result, isA<InvalidInputClassification>());
|
||||
final invalid = result as InvalidInputClassification;
|
||||
expect(invalid.reason, InvalidReason.emptyInput);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseSharedIntentUrl', () {
|
||||
test('parses explicit https', () {
|
||||
final uri = parseSharedIntentUrl('https://weblibre.eu');
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri!.scheme, 'https');
|
||||
});
|
||||
|
||||
test('treats explicit non-http scheme as non-url', () {
|
||||
final uri = parseSharedIntentUrl('moz-extension://abc/index.html');
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
|
||||
test('treats explicit dotted non-http scheme as non-url', () {
|
||||
final uri = parseSharedIntentUrl('my.app://callback');
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
|
||||
test('treats plain sentence as non-url', () {
|
||||
final uri = parseSharedIntentUrl('This is WebLibre README.md');
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/utils/text_highlight.dart';
|
||||
|
||||
void main() {
|
||||
group('buildHighlightedText', () {
|
||||
const baseStyle = TextStyle(color: Colors.black);
|
||||
const highlightStyle = TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
const matchPrefix = '***';
|
||||
const matchSuffix = '***';
|
||||
|
||||
test('returns plain text when no highlights present', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello world',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 1);
|
||||
final span = result.children![0] as TextSpan;
|
||||
expect(span.text, 'Hello world');
|
||||
expect(span.style, baseStyle);
|
||||
});
|
||||
|
||||
test('highlights single match in middle of text', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello ***world***!',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 3);
|
||||
|
||||
final span1 = result.children![0] as TextSpan;
|
||||
expect(span1.text, 'Hello ');
|
||||
expect(span1.style, baseStyle);
|
||||
|
||||
final span2 = result.children![1] as TextSpan;
|
||||
expect(span2.text, 'world');
|
||||
expect(span2.style, highlightStyle);
|
||||
|
||||
final span3 = result.children![2] as TextSpan;
|
||||
expect(span3.text, '!');
|
||||
expect(span3.style, baseStyle);
|
||||
});
|
||||
|
||||
test('highlights multiple matches', () {
|
||||
final result = buildHighlightedText(
|
||||
'The ***quick*** brown ***fox***',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 4);
|
||||
|
||||
expect((result.children![0] as TextSpan).text, 'The ');
|
||||
expect((result.children![0] as TextSpan).style, baseStyle);
|
||||
|
||||
expect((result.children![1] as TextSpan).text, 'quick');
|
||||
expect((result.children![1] as TextSpan).style, highlightStyle);
|
||||
|
||||
expect((result.children![2] as TextSpan).text, ' brown ');
|
||||
expect((result.children![2] as TextSpan).style, baseStyle);
|
||||
|
||||
expect((result.children![3] as TextSpan).text, 'fox');
|
||||
expect((result.children![3] as TextSpan).style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles highlight at start of text', () {
|
||||
final result = buildHighlightedText(
|
||||
'***Hello*** world',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 2);
|
||||
|
||||
final span1 = result.children![0] as TextSpan;
|
||||
expect(span1.text, 'Hello');
|
||||
expect(span1.style, highlightStyle);
|
||||
|
||||
final span2 = result.children![1] as TextSpan;
|
||||
expect(span2.text, ' world');
|
||||
expect(span2.style, baseStyle);
|
||||
});
|
||||
|
||||
test('handles highlight at end of text', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello ***world***',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 2);
|
||||
|
||||
final span1 = result.children![0] as TextSpan;
|
||||
expect(span1.text, 'Hello ');
|
||||
expect(span1.style, baseStyle);
|
||||
|
||||
final span2 = result.children![1] as TextSpan;
|
||||
expect(span2.text, 'world');
|
||||
expect(span2.style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles entire text highlighted', () {
|
||||
final result = buildHighlightedText(
|
||||
'***Hello world***',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 1);
|
||||
|
||||
final span = result.children![0] as TextSpan;
|
||||
expect(span.text, 'Hello world');
|
||||
expect(span.style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles consecutive highlights', () {
|
||||
final result = buildHighlightedText(
|
||||
'***hello******world***',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 2);
|
||||
|
||||
expect((result.children![0] as TextSpan).text, 'hello');
|
||||
expect((result.children![0] as TextSpan).style, highlightStyle);
|
||||
|
||||
expect((result.children![1] as TextSpan).text, 'world');
|
||||
expect((result.children![1] as TextSpan).style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles empty highlight', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello ******world',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 3);
|
||||
|
||||
expect((result.children![0] as TextSpan).text, 'Hello ');
|
||||
expect((result.children![1] as TextSpan).text, '');
|
||||
expect((result.children![2] as TextSpan).text, 'world');
|
||||
});
|
||||
|
||||
test('handles unclosed prefix marker', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello ***world',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
// When suffix not found, highlight everything after prefix to end
|
||||
expect(result.children?.length, 2);
|
||||
|
||||
expect((result.children![0] as TextSpan).text, 'Hello ');
|
||||
expect((result.children![0] as TextSpan).style, baseStyle);
|
||||
expect((result.children![1] as TextSpan).text, 'world');
|
||||
expect((result.children![1] as TextSpan).style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles missing suffix marker', () {
|
||||
final result = buildHighlightedText(
|
||||
'***Hello',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 1);
|
||||
|
||||
final span = result.children![0] as TextSpan;
|
||||
expect(span.text, 'Hello');
|
||||
expect(span.style, highlightStyle);
|
||||
});
|
||||
|
||||
test('handles empty string', () {
|
||||
final result = buildHighlightedText(
|
||||
'',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 0);
|
||||
});
|
||||
|
||||
test('handles different prefix and suffix markers', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello <mark>world</mark>!',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
'<mark>',
|
||||
'</mark>',
|
||||
);
|
||||
|
||||
expect(result.children?.length, 3);
|
||||
|
||||
expect((result.children![0] as TextSpan).text, 'Hello ');
|
||||
expect((result.children![1] as TextSpan).text, 'world');
|
||||
expect((result.children![1] as TextSpan).style, highlightStyle);
|
||||
expect((result.children![2] as TextSpan).text, '!');
|
||||
});
|
||||
|
||||
// Note: Nested markers behavior is undefined and not a real-world FTS5 scenario
|
||||
// This test is skipped as the exact parsing behavior for nested markers
|
||||
// is implementation-specific and not guaranteed
|
||||
|
||||
test('handles null styles', () {
|
||||
final result = buildHighlightedText(
|
||||
'Hello ***world***!',
|
||||
null,
|
||||
null,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 3);
|
||||
|
||||
expect((result.children![0] as TextSpan).style, null);
|
||||
expect((result.children![1] as TextSpan).style, null);
|
||||
expect((result.children![2] as TextSpan).style, null);
|
||||
});
|
||||
|
||||
test('real-world FTS5 example', () {
|
||||
final result = buildHighlightedText(
|
||||
'Mozilla Developer Network (***MDN***) Web Docs',
|
||||
baseStyle,
|
||||
highlightStyle,
|
||||
matchPrefix,
|
||||
matchSuffix,
|
||||
);
|
||||
|
||||
expect(result.children?.length, 3);
|
||||
|
||||
expect(
|
||||
(result.children![0] as TextSpan).text,
|
||||
'Mozilla Developer Network (',
|
||||
);
|
||||
expect((result.children![1] as TextSpan).text, 'MDN');
|
||||
expect((result.children![1] as TextSpan).style, highlightStyle);
|
||||
expect((result.children![2] as TextSpan).text, ') Web Docs');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/utils/uri_input_parser.dart';
|
||||
import 'package:weblibre/utils/uri_policy.dart';
|
||||
|
||||
void main() {
|
||||
group('parseExplicitUri', () {
|
||||
test('parses supported explicit scheme', () {
|
||||
final uri = parseExplicitUri(
|
||||
'https://weblibre.eu/path',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
);
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri!.scheme, 'https');
|
||||
expect(uri.host, 'weblibre.eu');
|
||||
});
|
||||
|
||||
test('rejects unsupported explicit scheme', () {
|
||||
final uri = parseExplicitUri(
|
||||
'javascript:alert(1)',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
);
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
|
||||
test('rejects malformed explicit uri with required authority', () {
|
||||
final uri = parseExplicitUri(
|
||||
'https:///path',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
);
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseSchemelessWebHost', () {
|
||||
test('upgrades domain to https', () {
|
||||
final uri = parseSchemelessWebHost('weblibre.eu');
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri!.toString(), 'https://weblibre.eu');
|
||||
});
|
||||
|
||||
test('upgrades localhost to http', () {
|
||||
final uri = parseSchemelessWebHost('localhost:8080');
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri!.toString(), 'http://localhost:8080');
|
||||
});
|
||||
|
||||
test('rejects spaces in host candidate', () {
|
||||
expect(parseSchemelessWebHost('foo bar.com'), isNull);
|
||||
});
|
||||
|
||||
test('rejects invalid port range', () {
|
||||
expect(parseSchemelessWebHost('weblibre.eu:99999'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseUserInputUrl', () {
|
||||
test('parses schemeless host only when enabled', () {
|
||||
expect(
|
||||
parseUserInputUrl('weblibre.eu', policy: SchemePolicy.addressBarTyped),
|
||||
isNull,
|
||||
);
|
||||
|
||||
expect(
|
||||
parseUserInputUrl(
|
||||
'weblibre.eu',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
allowSchemelessHosts: true,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects control chars', () {
|
||||
expect(
|
||||
parseUserInputUrl(
|
||||
'example.com\x00path',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
allowSchemelessHosts: true,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('allows long persisted urls when max length is not enforced', () {
|
||||
final longPath = 'a' * 5000;
|
||||
final uri = parseUserInputUrl(
|
||||
'https://weblibre.eu/$longPath',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
);
|
||||
|
||||
expect(uri, isNotNull);
|
||||
});
|
||||
|
||||
test('rejects long user input urls when max length is enforced', () {
|
||||
final longPath = 'a' * 5000;
|
||||
final uri = parseUserInputUrl(
|
||||
'https://weblibre.eu/$longPath',
|
||||
policy: SchemePolicy.addressBarTyped,
|
||||
enforceMaxInputLength: true,
|
||||
);
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('redactUriCredentials', () {
|
||||
test('strips userInfo from URI', () {
|
||||
final uri = Uri.parse('https://user:pass@example.com/path');
|
||||
final redacted = redactUriCredentials(uri);
|
||||
|
||||
expect(redacted.userInfo, isEmpty);
|
||||
expect(redacted.host, 'example.com');
|
||||
expect(redacted.path, '/path');
|
||||
expect(redacted.toString(), 'https://example.com/path');
|
||||
});
|
||||
|
||||
test('is no-op for URIs without credentials', () {
|
||||
final uri = Uri.parse('https://example.com/path');
|
||||
final redacted = redactUriCredentials(uri);
|
||||
|
||||
expect(identical(redacted, uri), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidHostCandidate', () {
|
||||
test('rejects single-label hosts', () {
|
||||
expect(isValidHostCandidate('example'), isFalse);
|
||||
expect(isValidHostCandidate('intranet'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('containsControlChars', () {
|
||||
test('detects null bytes', () {
|
||||
expect(containsControlChars('example\x00.com'), isTrue);
|
||||
});
|
||||
|
||||
test('detects C1 control chars', () {
|
||||
expect(containsControlChars('example\u0080.com'), isTrue);
|
||||
expect(containsControlChars('example\u009F.com'), isTrue);
|
||||
});
|
||||
|
||||
test('allows normal text', () {
|
||||
expect(containsControlChars('example.com'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user