import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:mews/mews/document.dart'; import 'package:mews/mews/html.dart'; /// Ordinary web markup: a doctype, unclosed tags, text loose inside wrappers, /// and plenty the Mews subset has nothing to draw for. const webPage = ''' A Journal
Loose text in a div.

On old phones

Notes on old phones and the about page.

Worth keeping.
  kept as is
ModelPrice
3310Ten

'''; /// Every bit of readable text in a page, so a test can say what survived the /// conversion and what did not. String textOf(List blocks) { final parts = []; void addInline(List runs) { for (final run in runs) { switch (run) { case MewsText(:final text): parts.add(text); case MewsEmphasis(:final children): addInline(children); case MewsLink(:final children): addInline(children); case MewsLineBreak(): break; } } } void addBlocks(List blocks) { for (final block in blocks) { switch (block) { case HeadingBlock(:final inline): addInline(inline); case ParagraphBlock(:final inline): addInline(inline); case AddressBlock(:final inline): addInline(inline); case QuoteBlock(:final children): addBlocks(children); case PreBlock(:final text): parts.add(text); case ListBlock(:final items): for (final item in items) { addBlocks(item.blocks); } case DefinitionListBlock(:final items): for (final item in items) { addInline(item.term); addInline(item.definition); } case TableBlock(:final rows): for (final row in rows) { for (final cell in row.cells) { addInline(cell.inline); } } case ImageBlock(:final alt): parts.add(alt); case RuleBlock(): break; case FormBlock(): break; } } } addBlocks(blocks); return parts.join(' '); } ({MewsPage page, bool declaresMews}) convert(String source, {String url = 'https://example.com/journal'}) => convertHtmlPage(url: url, source: source); void main() { test('converts a web page into the blocks Mews supports', () { final page = convert(webPage).page; final kinds = page.body.map((b) => b.runtimeType.toString()).toList(); expect(page.title, 'A Journal'); expect(page.lang, 'en'); expect(kinds, contains('HeadingBlock')); expect(kinds, contains('ParagraphBlock')); expect(kinds, contains('ListBlock')); expect(kinds, contains('QuoteBlock')); expect(kinds, contains('PreBlock')); expect(kinds, contains('TableBlock')); expect(kinds, contains('RuleBlock')); }); test('a converted page is marked as a simplified view', () { expect(convert(webPage).page.specVersion, htmlSpecVersion); // The marker is what makes a page Mews, and this one never claimed it. expect(convert(webPage).declaresMews, isFalse); }); test('a page claiming the Mews marker is told apart', () { const claimed = 'T' '' '

Never closed'; expect(convert(claimed).declaresMews, isTrue); }); test('keeps text sitting directly inside an unknown wrapper', () { final page = convert(webPage).page; final text = page.body .whereType() .expand((b) => b.inline.whereType()) .map((t) => t.text) .join(' '); expect(text, contains('Loose text in a div.')); }); test('closes tags the page left open', () { final page = convert('

One

Two

  • A
  • B
').page; expect(page.body.whereType().length, 2); expect(page.body.whereType().single.items.length, 2); }); test('leaves out markup that is not content', () { final all = textOf(convert(webPage).page.body); expect(all, isNot(contains('ignore.me'))); expect(all, isNot(contains('color: red'))); expect(all, isNot(contains('Press me'))); expect(all, isNot(contains('circle'))); // A nav is a list of real links, so it is kept rather than stripped. expect(all, contains('Home')); // The table's cells come through. expect(all, contains('3310')); }); test('resolves a relative link against the page address', () { final page = convert(webPage).page; final links = page.body .whereType() .expand((b) => b.inline.whereType()) .map((l) => l.href); expect(links, contains('https://example.com/about.html')); }); test('an image on another site falls back to its alt text', () { const source = 'A chart' 'Ours'; final images = convert(source).page.body.whereType().toList(); expect(images.first.allowed, isFalse); expect(images.first.alt, 'A chart'); expect(images.last.allowed, isTrue); }); test('an attribute no XML name allows does not stop the page', () { // The wrapper in webPage carries @click and v-bind:href. expect(() => convert(webPage), returnsNormally); expect(convert(webPage).page.body, isNotEmpty); }); test('reads the charset the document declares, not the one assumed', () { const html = 'T' '

Beschädigte Seite

'; final page = convertHtmlPage( url: 'https://example.com/', bytes: utf8.encode(html)) .page; final text = page.body .whereType() .expand((b) => b.inline.whereType()) .map((t) => t.text) .join(); expect(text, contains('Beschädigte Seite')); }); test('a nested table becomes a table of its own', () { // Old web layout markup: a table inside a cell of another. const source = '' '' '' '
Outer headOuter cell
Holds one' '
Inner aInner b
' '
Beside it
'; final tables = convert(source).page.body.whereType().toList(); expect(tables.length, 2); String cells(TableBlock t) => t.rows .map((r) => r.cells.map((c) => textOf([ParagraphBlock(c.inline)])).join('|')) .join(' / '); // The outer table keeps its own two rows; the inner row is not hoisted // into it, and the inner cells do not run together in the cell's text. expect(tables.first.rows.length, 2); expect(cells(tables.first), contains('Outer head')); expect(cells(tables.first), contains('Holds one')); expect(cells(tables.first), contains('Beside it')); expect(cells(tables.first), isNot(contains('Inner a'))); // The inner table stands on its own, with its content intact. expect(tables.last.rows.length, 1); expect(cells(tables.last), contains('Inner a')); expect(cells(tables.last), contains('Inner b')); }); test('leaves a form out but keeps what it wrapped', () { const source = '' '
' '' '' '' '' '' '
' '

Wrapped heading

' '

Real content inside a form.

' ''; final page = convert(source).page; final text = textOf(page.body); // No form survives, so nothing in the view invites an answer the reader // cannot send. expect(page.body.whereType(), isEmpty); expect(text, isNot(contains('Search'))); expect(text, isNot(contains('Titles'))); expect(text, isNot(contains('Draft'))); expect(text, isNot(contains('Go'))); // The form's wrapper goes, its content stays. expect(page.body.whereType(), isNotEmpty); expect(text, contains('Wrapped heading')); expect(text, contains('Real content inside a form.')); }); test('markup with nothing in it still yields a page', () { final page = convert('').page; expect(page.url, 'https://example.com/journal'); expect(page.body, isEmpty); }); }