def setUp(self): self.visitor = KumaVisitor()
def setUp(self): self.visitor = KumaVisitor() self.spec = self.get_instance('Specification', 'css3_backgrounds')
class TestVisitor(TestHTMLVisitor): def setUp(self): self.visitor = KumaVisitor() def assert_kumascript( self, text, name, args, scope, known=True, issues=None): parsed = kumascript_grammar['kumascript'].parse(text) self.visitor.scope = scope ks = self.visitor.visit(parsed) self.assertIsInstance(ks, KumaScript) self.assertEqual(ks.name, name) self.assertEqual(ks.args, args) self.assertEqual(ks.known, known) self.assertEqual(ks.issues, issues or []) def test_kumascript_no_args(self): self.assert_kumascript( '{{CompatNo}}', 'CompatNo', [], 'compatibility support') def test_kumascript_no_parens_and_spaces(self): self.assert_kumascript( '{{ CompatNo }}', 'CompatNo', [], 'compatibility support') def test_kumascript_empty_parens(self): self.assert_kumascript( '{{CompatNo()}}', 'CompatNo', [], 'compatibility support') def test_kumascript_one_arg(self): self.assert_kumascript( '{{cssxref("-moz-border-image")}}', 'cssxref', ['-moz-border-image'], 'footnote') def test_kumascript_one_arg_no_quotes(self): self.assert_kumascript( '{{CompatGeckoDesktop(27)}}', 'CompatGeckoDesktop', ['27'], 'compatibility support') def test_kumascript_three_args(self): self.get_instance('Specification', 'css3_backgrounds') self.assert_kumascript( ("{{SpecName('CSS3 Backgrounds', '#the-background-size'," " 'background-size')}}"), 'SpecName', ['CSS3 Backgrounds', '#the-background-size', 'background-size'], 'specification name') def test_kumascript_empty_string(self): # https://developer.mozilla.org/en-US/docs/Web/API/MIDIConnectionEvent raw = "{{SpecName('', '#midiconnection')}}" name = 'SpecName' args = ['', '#midiconnection'] issue = ( 'specname_blank_key', 0, 35, {'name': name, 'args': args, 'scope': 'specification name', 'kumascript': '{{SpecName("", "#midiconnection")}}'}) self.assert_kumascript( raw, name, args, 'specification name', issues=[issue]) def test_kumascript_unknown(self): issue = ( 'unknown_kumascript', 0, 10, {'name': 'CSSRef', 'args': [], 'scope': 'footnote', 'kumascript': '{{CSSRef}}'}) self.assert_kumascript( '{{CSSRef}}', 'CSSRef', [], scope='footnote', known=False, issues=[issue]) def test_kumascript_in_html(self): html = """\ <tr> <td>{{SpecName('CSS3 Display', '#display', 'display')}}</td> <td>{{Spec2('CSS3 Display')}}</td> <td>Added the <code>run-in</code> and <code>contents</code> values.</td> </tr>""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) tr = out[0] self.assertEqual(tr.tag, 'tr') texts = [None] * 4 texts[0], td1, texts[1], td2, texts[2], td3, texts[3] = tr.children for text in texts: self.assertIsInstance(text, HTMLText) self.assertFalse(text.cleaned) self.assertEqual(td1.tag, 'td') self.assertEqual(len(td1.children), 1) self.assertIsInstance(td1.children[0], SpecName) self.assertEqual(td2.tag, 'td') self.assertEqual(len(td2.children), 1) self.assertIsInstance(td2.children[0], Spec2) self.assertEqual(td3.tag, 'td') text1, code1, text2, code2, text3 = td3.children self.assertEqual(str(text1), 'Added the') self.assertEqual(str(code1), '<code>run-in</code>') self.assertEqual(str(text2), 'and') self.assertEqual(str(code2), '<code>contents</code>') self.assertEqual(str(text3), 'values.') def test_kumascript_and_text_and_HTML(self): html = """\ <td> Add the {{ xref_csslength() }} value and allows it to be applied to element with a {{ cssxref("display") }} type of <code>table-cell</code>. </td>""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) tr = out[0] self.assertEqual(tr.tag, 'td') txts = [None] * 4 ks = [None] * 2 txts[0], ks[0], txts[1], ks[1], txts[2], code, txts[3] = tr.children for text in txts: self.assertIsInstance(text, HTMLText) self.assertTrue(text.cleaned) self.assertEqual('Add the', str(txts[0])) self.assertEqual( 'value and allows it to be applied to element with a', str(txts[1])) self.assertEqual('type of', str(txts[2])) self.assertEqual('.', str(txts[3])) self.assertEqual('{{xref_csslength}}', str(ks[0])) self.assertEqual('{{cssxref("display")}}', str(ks[1])) self.assertEqual('<code>table-cell</code>', str(code)) def assert_compat_version(self, html, cls, version): """Check that Compat* KumaScript is parsed correctly.""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) ks = out[0] self.assertIsInstance(ks, cls) self.assertEqual(version, ks.version) def test_compatchrome(self): self.assert_compat_version( '{{CompatChrome("10.0")}}', CompatChrome, '10.0') def test_compatie(self): self.assert_compat_version( '{{CompatIE("9")}}', CompatIE, '9.0') def test_compatopera(self): self.assert_compat_version( '{{CompatOpera("9")}}', CompatOpera, '9.0') def test_compatoperamobile(self): self.assert_compat_version( '{{CompatOperaMobile("11.5")}}', CompatOperaMobile, '11.5') def test_compatsafari(self): self.assert_compat_version( '{{CompatSafari("2")}}', CompatSafari, '2.0') def assert_a(self, html, converted, issues=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) a = out[0] self.assertEqual('a', a.tag) self.assertEqual(converted, a.to_html()) self.assertEqual(issues or [], self.visitor.issues) def test_a_missing(self): # https://developer.mozilla.org/en-US/docs/Web/CSS/flex issues = [ ('unexpected_attribute', 3, 13, {'node_type': 'a', 'ident': 'name', 'value': 'bc1', 'expected': 'the attribute href'}), ('missing_attribute', 0, 14, {'node_type': 'a', 'ident': 'href'})] self.assert_a( '<a name="bc1">[1]</a>', '<a>[1]</a>', issues=issues) def test_a_MDN_relative(self): # https://developer.mozilla.org/en-US/docs/Web/CSS/image self.assert_a( '<a href="/en-US/docs/Web/CSS/CSS3">CSS3</a>', ('<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS3">' 'CSS3</a>')) def test_a_external(self): # https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API self.assert_a( ('<a href="https://dvcs.w3.org/hg/speech-api/raw-file/tip/' 'speechapi.html" class="external external-icon">Web Speech API' '</a>'), ('<a href="https://dvcs.w3.org/hg/speech-api/raw-file/tip/' 'speechapi.html">Web Speech API</a>')) def test_a_bad_class(self): # https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagNameNS self.assert_a( ('<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5"' ' class="link-https"' ' title="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5">' 'comment from Henri Sivonen about the change</a>'), ('<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5"' '>comment from Henri Sivonen about the change</a>'), [('unexpected_attribute', 65, 83, {'node_type': 'a', 'ident': 'class', 'value': 'link-https', 'expected': 'the attribute href'})])
def setUp(self): self.feature = self.get_instance('Feature', 'web-css-background-size') self.visitor = KumaVisitor() self.version = self.get_instance('Version', ('firefox_desktop', '1.0'))
class TestSpecSectionExtractor(TestCase): def setUp(self): self.visitor = KumaVisitor() self.spec = self.get_instance('Specification', 'css3_backgrounds') def construct_html( self, header=None, pre_table='', row=None, post_table=''): """Create a specification section with overrides.""" header = header or """\ <h2 id="Specifications" name="Specifications">Specifications</h2>""" row = row or """\ <tr> <td>{{SpecName('CSS3 Backgrounds', '#the-background-size',\ 'background-size')}}</td> <td>{{Spec2('CSS3 Backgrounds')}}</td> <td></td> </tr>""" html = header + pre_table + """\ <table class="standard-table"> <thead> <tr> <th scope="col">Specification</th> <th scope="col">Status</th> <th scope="col">Comment</th> </tr> </thead> <tbody> """ + row + """ </tbody> </table> """ + post_table return html def get_default_spec(self): return { 'section.note': '', 'section.subpath': '#the-background-size', 'section.name': 'background-size', 'specification.mdn_key': 'CSS3 Backgrounds', 'section.id': None, 'specification.id': self.spec.id, } def assert_extract(self, html, specs=None, issues=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) extractor = SpecSectionExtractor(elements=out) extracted = extractor.extract() self.assertEqual(extracted['specs'], specs or []) self.assertEqual(extracted['issues'], issues or []) def test_standard(self): html = self.construct_html() expected_spec = self.get_default_spec() self.assert_extract(html, [expected_spec]) def test_key_mismatch(self): spec = self.get_instance('Specification', 'css3_ui') spec_row = '''\ <tr> <td>{{ SpecName('CSS3 UI', '#cursor', 'cursor') }}</td> <td>{{ Spec2('CSS3 Basic UI') }}</td> <td>Addition of several keywords and the positioning syntax for\ <code>url()</code>.</td> </tr>''' html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': ( 'Addition of several keywords and the positioning syntax for' ' <code>url()</code>.'), 'section.subpath': '#cursor', 'section.name': 'cursor', 'specification.mdn_key': 'CSS3 UI', 'section.id': None, 'specification.id': spec.id}] issues = [ ('unknown_spec', 309, 337, {'key': u'CSS3 Basic UI'}), ('spec_mismatch', 305, 342, {'spec2_key': 'CSS3 Basic UI', 'specname_key': 'CSS3 UI'})] self.assert_extract(html, expected_specs, issues) def test_known_spec(self): spec = self.get_instance('Specification', 'css3_backgrounds') self.create(Section, specification=spec) expected_spec = self.get_default_spec() expected_spec['specification.id'] = spec.id self.assert_extract(self.construct_html(), [expected_spec]) def test_known_spec_and_section(self): section = self.get_instance('Section', 'background-size') spec = section.specification expected_spec = self.get_default_spec() expected_spec['specification.id'] = spec.id expected_spec['section.id'] = section.id self.assert_extract(self.construct_html(), [expected_spec], []) def test_es1(self): # en-US/docs/Web/JavaScript/Reference/Operators/this es1 = self.get_instance('Specification', 'es1') spec_row = """\ <tr> <td>ECMAScript 1st Edition.</td> <td>Standard</td> <td>Initial definition.</td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': 'Initial definition.', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': 'ES1', 'section.id': None, 'specification.id': es1.id}] issues = [ ('specname_converted', 251, 274, {'key': 'ES1', 'original': 'ECMAScript 1st Edition.'}), ('spec2_converted', 286, 294, {'key': 'ES1', 'original': 'Standard'})] self.assert_extract(html, expected_specs, issues) def test_nonstandard(self): # https://developer.mozilla.org/en-US/docs/Web/API/Promise (fixed) spec_row = """\ <tr> <td><a href="https://github.com/domenic/promises-unwrapping">\ domenic/promises-unwrapping</a></td> <td>Draft</td> <td>Standardization work is taking place here.</td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': 'Standardization work is taking place here.', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': '', 'section.id': None, 'specification.id': None}] issues = [ ('tag_dropped', 252, 309, {'tag': 'a', 'scope': 'specification name'}), ('specname_not_kumascript', 309, 336, {'original': 'domenic/promises-unwrapping'}), ('spec2_converted', 353, 358, {'key': '', 'original': 'Draft'})] self.assert_extract(html, expected_specs, issues) def test_spec2_td_no_spec(self): # https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput spec = self.get_instance('Specification', 'css3_backgrounds') spec_row = """\ <tr> <td>{{SpecName('CSS3 Backgrounds', '#the-background-size',\ 'background-size')}}</td> <td>{{Spec2()}}</td> <td></td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': '', 'section.subpath': '#the-background-size', 'section.name': 'background-size', 'specification.mdn_key': 'CSS3 Backgrounds', 'section.id': None, 'specification.id': spec.id}] issues = [( 'kumascript_wrong_args', 340, 351, {'name': 'Spec2', 'args': [], 'scope': 'specification maturity', 'kumascript': '{{Spec2}}', 'min': 1, 'max': 1, 'count': 0, 'arg_names': ['SpecKey'], 'arg_count': '0 arguments', 'arg_spec': 'exactly 1 argument (SpecKey)'})] self.assert_extract(html, expected_specs, issues) def test_specrow_empty(self): # https://developer.mozilla.org/en-US/docs/Web/API/BluetoothGattService spec_row = """\ <tr> <td> </td> <td> </td> <td> </td> </tr>""" html = self.construct_html(row=spec_row) expected_spec = { 'section.note': '', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': '', 'section.id': None, 'specification.id': None} issues = [ ('specname_omitted', 248, 258, {}), ('spec2_omitted', 262, 272, {})] self.assert_extract(html, [expected_spec], issues) def test_whynospec(self): # https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/initAnimationEvent pre_table = """ <p> {{WhyNoSpecStart}} This method is non-standard and not part of any specification, though it was present in early drafts of {{SpecName("CSS3 Animations")}}. {{WhyNoSpecEnd}} </p>""" html = self.construct_html(pre_table=pre_table) self.assert_extract(html, [self.get_default_spec()]) def test_pre_table_content(self): pre_table = """ <p> This method is non-standard and not part of any specification, though it was present in early drafts of {{SpecName("CSS3 Animations")}}. </p>""" html = self.construct_html(pre_table=pre_table) issues = [('skipped_content', 66, 211, {})] self.assert_extract(html, [self.get_default_spec()], issues) def test_post_table_content(self): post_table = ( '<p>You may also be interested in the user group posts.</p>') html = self.construct_html(post_table=post_table) issues = [('skipped_content', 413, 471, {})] self.assert_extract(html, [self.get_default_spec()], issues) def test_h2_discards_extra(self): h2_extra = ( '<h2 id="Specifications" name="Specifications" extra="crazy">' 'Specifications</h2>') html = self.construct_html(header=h2_extra) self.assert_extract(html, [self.get_default_spec()]) def test_h2_browser_compat(self): # Common bug from copying from Browser Compatibility section h2_browser_compat = ( '<h2 id="Browser_compatibility" name="Browser_compatibility">' 'Specifications</h2>') html = self.construct_html(header=h2_browser_compat) issues = [ ('spec_h2_id', 4, 30, {'h2_id': 'Browser_compatibility'}), ('spec_h2_name', 31, 59, {'h2_name': 'Browser_compatibility'})] self.assert_extract(html, [self.get_default_spec()], issues)
class TestCompatSectionExtractor(TestCase): def setUp(self): self.feature = self.get_instance('Feature', 'web-css-background-size') self.visitor = KumaVisitor() self.version = self.get_instance('Version', ('firefox_desktop', '1.0')) def construct_html( self, header=None, pre_table=None, feature=None, browser=None, support=None, after_table=None): """Create a basic compatibility section.""" return """\ {header} {pre_table} <div id="compat-desktop"> <table class="compat-table"> <tbody> <tr> <th>Feature</th> <th>{browser}</th> </tr> <tr> <td>{feature}</td> <td>{support}</td> </tr> </tbody> </table> </div> {after_table} """.format( header=header or ( '<h2 id="Browser_compatibility">Browser compatibility</h2>'), pre_table=pre_table or '<div>{{CompatibilityTable}}</div>', browser=browser or 'Firefox', feature=feature or '<code>contain</code> and <code>cover</code>', support=support or '1.0', after_table=after_table or '') def get_default_compat_div(self): browser_id = self.version.browser_id version_id = self.version.id return { 'name': u'desktop', 'browsers': [{ 'id': browser_id, 'name': 'Firefox for Desktop', 'slug': 'firefox_desktop'}], 'versions': [{ 'browser': browser_id, 'id': version_id, 'version': '1.0'}], 'features': [{ 'id': '_contain and cover', 'name': '<code>contain</code> and <code>cover</code>', 'slug': 'web-css-background-size_contain_and_cover'}], 'supports': [{ 'feature': '_contain and cover', 'id': '__contain and cover-%s' % version_id, 'support': 'yes', 'version': version_id}]} def assert_extract( self, html, compat_divs=None, footnotes=None, issues=None, embedded=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) extractor = CompatSectionExtractor(feature=self.feature, elements=out) extracted = extractor.extract() self.assertEqual(extracted['compat_divs'], compat_divs or []) self.assertEqual(extracted['footnotes'], footnotes or {}) self.assertEqual(extracted['issues'], issues or []) self.assertEqual(extracted['embedded'], embedded or []) def test_standard(self): html = self.construct_html() expected = self.get_default_compat_div() self.assert_extract(html, [expected]) def test_unknown_browser(self): html = self.construct_html(browser='Fire') expected = self.get_default_compat_div() expected['browsers'][0] = { 'id': '_Fire', 'name': 'Fire', 'slug': '_Fire'} expected['versions'][0] = { 'id': '_Fire-1.0', 'version': '1.0', 'browser': '_Fire'} expected['supports'][0] = { 'id': u'__contain and cover-_Fire-1.0', 'support': 'yes', 'feature': '_contain and cover', 'version': '_Fire-1.0'} issue = ('unknown_browser', 205, 218, {'name': 'Fire'}) self.assert_extract(html, [expected], issues=[issue]) def test_wrong_first_column_header(self): # All known pages use "Feature" for first column, but be ready html = self.construct_html() html = html.replace('<th>Feature</th>', '<th>Features</th>') expected = self.get_default_compat_div() issue = ('feature_header', 180, 197, {'header': 'Features'}) self.assert_extract(html, [expected], issues=[issue]) def test_footnote(self): html = self.construct_html( support='1.0 [1]', after_table='<p>[1] This is a footnote.</p>') expected = self.get_default_compat_div() expected['supports'][0]['footnote'] = 'This is a footnote.' expected['supports'][0]['footnote_id'] = ('1', 322, 325) self.assert_extract(html, [expected]) def test_footnote_mismatch(self): html = self.construct_html( support='1.0 [1]', after_table='<p>[2] Oops, footnote ID is wrong.</p>') expected = self.get_default_compat_div() expected['supports'][0]['footnote_id'] = ('1', 322, 325) footnotes = {'2': ('Oops, footnote ID is wrong.', 374, 412)} issues = [ ('footnote_missing', 322, 325, {'footnote_id': '1'}), ('footnote_unused', 374, 412, {'footnote_id': '2'})] self.assert_extract( html, [expected], footnotes=footnotes, issues=issues) def test_extra_row_cell(self): # https://developer.mozilla.org/en-US/docs/Web/JavaScript/ # Reference/Global_Objects/WeakSet, March 2015 html = self.construct_html() html = html.replace( '<td>1.0</td>', '<td>1.0</td><td>{{CompatUnknown()}}</td>') self.assertTrue('CompatUnknown' in html) expected = self.get_default_compat_div() issue = ('extra_cell', 326, 354, {}) self.assert_extract(html, [expected], issues=[issue]) def test_compat_mobile_table(self): mobile = """ <div id="compat-mobile"> <table class="compat-table"> <tbody> <tr><th>Feature</th><th>Safari Mobile</th></tr> <tr> <td><code>contain</code> and <code>cover</code></td> <td>1.0 [1]</td> </tr> </tbody> </table> </div> <p></p> <p>[1] It's really supported.</p> """ html = self.construct_html(after_table=mobile) expected_desktop = self.get_default_compat_div() expected_mobile = { 'name': 'mobile', 'browsers': [{ 'id': '_Safari for iOS', 'name': 'Safari for iOS', 'slug': '_Safari for iOS', }], 'features': [{ 'id': '_contain and cover', 'name': '<code>contain</code> and <code>cover</code>', 'slug': 'web-css-background-size_contain_and_cover', }], 'versions': [{ 'id': '_Safari for iOS-1.0', 'version': '1.0', 'browser': '_Safari for iOS', }], 'supports': [{ 'id': '__contain and cover-_Safari for iOS-1.0', 'feature': '_contain and cover', 'support': 'yes', 'version': '_Safari for iOS-1.0', 'footnote': "It's really supported.", 'footnote_id': ('1', 581, 584), }], } issue = ('unknown_browser', 465, 487, {'name': 'Safari Mobile'}) self.assert_extract( html, [expected_desktop, expected_mobile], issues=[issue]) def test_pre_content(self): header_plus = ( '<h2 id="Browser_compatibility">Browser compatibility</h2>' '<p>Here\'s some extra content.</p>') html = self.construct_html(header=header_plus) expected = self.get_default_compat_div() issue = ('skipped_content', 57, 90, {}) self.assert_extract(html, [expected], issues=[issue]) def test_feature_issue(self): html = self.construct_html( feature='<code>contain</code> and <code>cover</code> [1]') expected = self.get_default_compat_div() issue = ('footnote_feature', 300, 304, {}) self.assert_extract(html, [expected], issues=[issue]) def test_support_issue(self): html = self.construct_html(support='1.0 (or earlier)') expected = self.get_default_compat_div() issue = ('inline_text', 322, 334, {'text': '(or earlier)'}) self.assert_extract(html, [expected], issues=[issue]) def test_footnote_issue(self): html = self.construct_html(after_table="<p>Here's some text.</p>") expected = self.get_default_compat_div() issue = ('footnote_no_id', 370, 394, {}) self.assert_extract(html, [expected], issues=[issue]) def test_table_div_wraps_h3(self): # https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode html = self.construct_html() html = html.replace( '</div>', '<h3>Gecko Notes</h3><p>It rocks</p></div>') expected = self.get_default_compat_div() issues = [ ('skipped_content', 58, 126, {}), ('footnote_gap', 434, 438, {}), ('footnote_no_id', 418, 433, {})] self.assert_extract(html, [expected], issues=issues) def test_support_colspan_exceeds_table_width(self): # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent html = self.construct_html() html = html.replace('<td>1.0', '<td colspan="2">1.0') expected = self.get_default_compat_div() issue = ('cell_out_of_bounds', 314, 338, {}) self.assert_extract(html, [expected], issues=[issue]) def test_embedded(self): html = self.construct_html( after_table="<div>{{EmbedCompatTable('foo-bar')}}</div>") expected = self.get_default_compat_div() self.assert_extract(html, [expected], embedded=['foo-bar'])
class TestSpecSectionExtractor(TestCase): def setUp(self): self.visitor = KumaVisitor() self.spec = self.get_instance('Specification', 'css3_backgrounds') def construct_html(self, header=None, pre_table='', row=None, post_table=''): """Create a specification section with overrides.""" header = header or """\ <h2 id="Specifications" name="Specifications">Specifications</h2>""" row = row or """\ <tr> <td>{{SpecName('CSS3 Backgrounds', '#the-background-size',\ 'background-size')}}</td> <td>{{Spec2('CSS3 Backgrounds')}}</td> <td></td> </tr>""" html = header + pre_table + """\ <table class="standard-table"> <thead> <tr> <th scope="col">Specification</th> <th scope="col">Status</th> <th scope="col">Comment</th> </tr> </thead> <tbody> """ + row + """ </tbody> </table> """ + post_table return html def get_default_spec(self): return { 'section.note': '', 'section.subpath': '#the-background-size', 'section.name': 'background-size', 'specification.mdn_key': 'CSS3 Backgrounds', 'section.id': None, 'specification.id': self.spec.id, } def assert_extract(self, html, specs=None, issues=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) extractor = SpecSectionExtractor(elements=out) extracted = extractor.extract() self.assertEqual(extracted['specs'], specs or []) self.assertEqual(extracted['issues'], issues or []) def test_standard(self): html = self.construct_html() expected_spec = self.get_default_spec() self.assert_extract(html, [expected_spec]) def test_key_mismatch(self): spec = self.get_instance('Specification', 'css3_ui') spec_row = '''\ <tr> <td>{{ SpecName('CSS3 UI', '#cursor', 'cursor') }}</td> <td>{{ Spec2('CSS3 Basic UI') }}</td> <td>Addition of several keywords and the positioning syntax for\ <code>url()</code>.</td> </tr>''' html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': ('Addition of several keywords and the positioning syntax for' ' <code>url()</code>.'), 'section.subpath': '#cursor', 'section.name': 'cursor', 'specification.mdn_key': 'CSS3 UI', 'section.id': None, 'specification.id': spec.id }] issues = [('unknown_spec', 309, 337, { 'key': u'CSS3 Basic UI' }), ('spec_mismatch', 305, 342, { 'spec2_key': 'CSS3 Basic UI', 'specname_key': 'CSS3 UI' })] self.assert_extract(html, expected_specs, issues) def test_known_spec(self): spec = self.get_instance('Specification', 'css3_backgrounds') self.create(Section, specification=spec) expected_spec = self.get_default_spec() expected_spec['specification.id'] = spec.id self.assert_extract(self.construct_html(), [expected_spec]) def test_known_spec_and_section(self): section = self.get_instance('Section', 'background-size') spec = section.specification expected_spec = self.get_default_spec() expected_spec['specification.id'] = spec.id expected_spec['section.id'] = section.id self.assert_extract(self.construct_html(), [expected_spec], []) def test_es1(self): # en-US/docs/Web/JavaScript/Reference/Operators/this es1 = self.get_instance('Specification', 'es1') spec_row = """\ <tr> <td>ECMAScript 1st Edition.</td> <td>Standard</td> <td>Initial definition.</td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': 'Initial definition.', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': 'ES1', 'section.id': None, 'specification.id': es1.id }] issues = [('specname_converted', 251, 274, { 'key': 'ES1', 'original': 'ECMAScript 1st Edition.' }), ('spec2_converted', 286, 294, { 'key': 'ES1', 'original': 'Standard' })] self.assert_extract(html, expected_specs, issues) def test_nonstandard(self): # https://developer.mozilla.org/en-US/docs/Web/API/Promise (fixed) spec_row = """\ <tr> <td><a href="https://github.com/domenic/promises-unwrapping">\ domenic/promises-unwrapping</a></td> <td>Draft</td> <td>Standardization work is taking place here.</td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': 'Standardization work is taking place here.', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': '', 'section.id': None, 'specification.id': None }] issues = [('tag_dropped', 252, 309, { 'tag': 'a', 'scope': 'specification name' }), ('specname_not_kumascript', 309, 336, { 'original': 'domenic/promises-unwrapping' }), ('spec2_converted', 353, 358, { 'key': '', 'original': 'Draft' })] self.assert_extract(html, expected_specs, issues) def test_spec2_td_no_spec(self): # https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput spec = self.get_instance('Specification', 'css3_backgrounds') spec_row = """\ <tr> <td>{{SpecName('CSS3 Backgrounds', '#the-background-size',\ 'background-size')}}</td> <td>{{Spec2()}}</td> <td></td> </tr>""" html = self.construct_html(row=spec_row) expected_specs = [{ 'section.note': '', 'section.subpath': '#the-background-size', 'section.name': 'background-size', 'specification.mdn_key': 'CSS3 Backgrounds', 'section.id': None, 'specification.id': spec.id }] issues = [('kumascript_wrong_args', 340, 351, { 'name': 'Spec2', 'args': [], 'scope': 'specification maturity', 'kumascript': '{{Spec2}}', 'min': 1, 'max': 1, 'count': 0, 'arg_names': ['SpecKey'], 'arg_count': '0 arguments', 'arg_spec': 'exactly 1 argument (SpecKey)' })] self.assert_extract(html, expected_specs, issues) def test_specrow_empty(self): # https://developer.mozilla.org/en-US/docs/Web/API/BluetoothGattService spec_row = """\ <tr> <td> </td> <td> </td> <td> </td> </tr>""" html = self.construct_html(row=spec_row) expected_spec = { 'section.note': '', 'section.subpath': '', 'section.name': '', 'specification.mdn_key': '', 'section.id': None, 'specification.id': None } issues = [('specname_omitted', 248, 258, {}), ('spec2_omitted', 262, 272, {})] self.assert_extract(html, [expected_spec], issues) def test_whynospec(self): # https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/initAnimationEvent pre_table = """ <p> {{WhyNoSpecStart}} This method is non-standard and not part of any specification, though it was present in early drafts of {{SpecName("CSS3 Animations")}}. {{WhyNoSpecEnd}} </p>""" html = self.construct_html(pre_table=pre_table) self.assert_extract(html, [self.get_default_spec()]) def test_pre_table_content(self): pre_table = """ <p> This method is non-standard and not part of any specification, though it was present in early drafts of {{SpecName("CSS3 Animations")}}. </p>""" html = self.construct_html(pre_table=pre_table) issues = [('skipped_content', 66, 211, {})] self.assert_extract(html, [self.get_default_spec()], issues) def test_post_table_content(self): post_table = ( '<p>You may also be interested in the user group posts.</p>') html = self.construct_html(post_table=post_table) issues = [('skipped_content', 413, 471, {})] self.assert_extract(html, [self.get_default_spec()], issues) def test_h2_discards_extra(self): h2_extra = ( '<h2 id="Specifications" name="Specifications" extra="crazy">' 'Specifications</h2>') html = self.construct_html(header=h2_extra) self.assert_extract(html, [self.get_default_spec()]) def test_h2_browser_compat(self): # Common bug from copying from Browser Compatibility section h2_browser_compat = ( '<h2 id="Browser_compatibility" name="Browser_compatibility">' 'Specifications</h2>') html = self.construct_html(header=h2_browser_compat) issues = [('spec_h2_id', 4, 30, { 'h2_id': 'Browser_compatibility' }), ('spec_h2_name', 31, 59, { 'h2_name': 'Browser_compatibility' })] self.assert_extract(html, [self.get_default_spec()], issues)
class TestVisitor(TestHTMLVisitor): def setUp(self): self.visitor = KumaVisitor() def assert_kumascript( self, text, name, args, scope, known=True, issues=None): parsed = kumascript_grammar['kumascript'].parse(text) self.visitor.scope = scope ks = self.visitor.visit(parsed) self.assertIsInstance(ks, KumaScript) self.assertEqual(ks.name, name) self.assertEqual(ks.args, args) self.assertEqual(ks.known, known) self.assertEqual(ks.issues, issues or []) def test_kumascript_no_args(self): self.assert_kumascript( '{{CompatNo}}', 'CompatNo', [], 'compatibility support') def test_kumascript_no_parens_and_spaces(self): self.assert_kumascript( '{{ CompatNo }}', 'CompatNo', [], 'compatibility support') def test_kumascript_empty_parens(self): self.assert_kumascript( '{{CompatNo()}}', 'CompatNo', [], 'compatibility support') def test_kumascript_one_arg(self): self.assert_kumascript( '{{cssxref("-moz-border-image")}}', 'cssxref', ['-moz-border-image'], 'footnote') def test_kumascript_one_arg_no_quotes(self): self.assert_kumascript( '{{CompatGeckoDesktop(27)}}', 'CompatGeckoDesktop', ['27'], 'compatibility support') def test_kumascript_three_args(self): self.get_instance('Specification', 'css3_backgrounds') self.assert_kumascript( ("{{SpecName('CSS3 Backgrounds', '#the-background-size'," " 'background-size')}}"), "SpecName", ["CSS3 Backgrounds", "#the-background-size", "background-size"], 'specification name') def test_kumascript_empty_string(self): # https://developer.mozilla.org/en-US/docs/Web/API/MIDIConnectionEvent raw = "{{SpecName('', '#midiconnection')}}" name = "SpecName" args = ['', '#midiconnection'] issue = ( 'specname_blank_key', 0, 35, {'name': name, 'args': args, 'scope': 'specification name', 'kumascript': '{{SpecName("", "#midiconnection")}}'}) self.assert_kumascript( raw, name, args, 'specification name', issues=[issue]) def test_kumascript_unknown(self): issue = ( 'unknown_kumascript', 0, 10, {'name': 'CSSRef', 'args': [], 'scope': 'footnote', 'kumascript': '{{CSSRef}}'}) self.assert_kumascript( "{{CSSRef}}", "CSSRef", [], scope='footnote', known=False, issues=[issue]) def test_kumascript_in_html(self): html = """\ <tr> <td>{{SpecName('CSS3 Display', '#display', 'display')}}</td> <td>{{Spec2('CSS3 Display')}}</td> <td>Added the <code>run-in</code> and <code>contents</code> values.</td> </tr>""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) tr = out[0] self.assertEqual(tr.tag, 'tr') texts = [None] * 4 texts[0], td1, texts[1], td2, texts[2], td3, texts[3] = tr.children for text in texts: self.assertIsInstance(text, HTMLText) self.assertFalse(text.cleaned) self.assertEqual(td1.tag, 'td') self.assertEqual(len(td1.children), 1) self.assertIsInstance(td1.children[0], SpecName) self.assertEqual(td2.tag, 'td') self.assertEqual(len(td2.children), 1) self.assertIsInstance(td2.children[0], Spec2) self.assertEqual(td3.tag, 'td') text1, code1, text2, code2, text3 = td3.children self.assertEqual(str(text1), 'Added the') self.assertEqual(str(code1), '<code>run-in</code>') self.assertEqual(str(text2), 'and') self.assertEqual(str(code2), '<code>contents</code>') self.assertEqual(str(text3), 'values.') def test_kumascript_and_text_and_HTML(self): html = """\ <td> Add the {{ xref_csslength() }} value and allows it to be applied to element with a {{ cssxref("display") }} type of <code>table-cell</code>. </td>""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) tr = out[0] self.assertEqual(tr.tag, 'td') txts = [None] * 4 ks = [None] * 2 txts[0], ks[0], txts[1], ks[1], txts[2], code, txts[3] = tr.children for text in txts: self.assertIsInstance(text, HTMLText) self.assertTrue(text.cleaned) self.assertEqual('Add the', str(txts[0])) self.assertEqual( 'value and allows it to be applied to element with a', str(txts[1])) self.assertEqual('type of', str(txts[2])) self.assertEqual('.', str(txts[3])) self.assertEqual('{{xref_csslength}}', str(ks[0])) self.assertEqual('{{cssxref("display")}}', str(ks[1])) self.assertEqual('<code>table-cell</code>', str(code)) def assert_compat_version(self, html, cls, version): """Check that Compat* KumaScript is parsed correctly""" parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) ks = out[0] self.assertIsInstance(ks, cls) self.assertEqual(version, ks.version) def test_compatchrome(self): self.assert_compat_version( '{{CompatChrome("10.0")}}', CompatChrome, '10.0') def test_compatie(self): self.assert_compat_version( '{{CompatIE("9")}}', CompatIE, '9.0') def test_compatopera(self): self.assert_compat_version( '{{CompatOpera("9")}}', CompatOpera, '9.0') def test_compatoperamobile(self): self.assert_compat_version( '{{CompatOperaMobile("11.5")}}', CompatOperaMobile, '11.5') def test_compatsafari(self): self.assert_compat_version( '{{CompatSafari("2")}}', CompatSafari, '2.0') def assert_a(self, html, converted, issues=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) self.assertEqual(len(out), 1) a = out[0] self.assertEqual('a', a.tag) self.assertEqual(converted, a.to_html()) self.assertEqual(issues or [], self.visitor.issues) def test_a_missing(self): # https://developer.mozilla.org/en-US/docs/Web/CSS/flex issues = [ ('unexpected_attribute', 3, 13, {'node_type': 'a', 'ident': 'name', 'value': 'bc1', 'expected': 'the attribute href'}), ('missing_attribute', 0, 14, {'node_type': 'a', 'ident': 'href'})] self.assert_a( '<a name="bc1">[1]</a>', '<a>[1]</a>', issues=issues) def test_a_MDN_relative(self): # https://developer.mozilla.org/en-US/docs/Web/CSS/image self.assert_a( '<a href="/en-US/docs/Web/CSS/CSS3">CSS3</a>', ('<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS3">' 'CSS3</a>')) def test_a_external(self): # https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API self.assert_a( ('<a href="https://dvcs.w3.org/hg/speech-api/raw-file/tip/' 'speechapi.html" class="external external-icon">Web Speech API' '</a>'), ('<a href="https://dvcs.w3.org/hg/speech-api/raw-file/tip/' 'speechapi.html">Web Speech API</a>')) def test_a_bad_class(self): # https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagNameNS self.assert_a( ('<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5"' ' class="link-https"' ' title="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5">' 'comment from Henri Sivonen about the change</a>'), ('<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=542185#c5"' '>comment from Henri Sivonen about the change</a>'), [('unexpected_attribute', 65, 83, {'node_type': 'a', 'ident': 'class', 'value': 'link-https', 'expected': 'the attribute href'})])
class TestCompatSectionExtractor(TestCase): def setUp(self): self.feature = self.get_instance('Feature', 'web-css-background-size') self.visitor = KumaVisitor() self.version = self.get_instance('Version', ('firefox_desktop', '1.0')) def construct_html(self, header=None, pre_table=None, feature=None, browser=None, support=None, after_table=None): """Create a basic compatibility section.""" return """\ {header} {pre_table} <div id="compat-desktop"> <table class="compat-table"> <tbody> <tr> <th>Feature</th> <th>{browser}</th> </tr> <tr> <td>{feature}</td> <td>{support}</td> </tr> </tbody> </table> </div> {after_table} """.format(header=header or ('<h2 id="Browser_compatibility">Browser compatibility</h2>'), pre_table=pre_table or '<div>{{CompatibilityTable}}</div>', browser=browser or 'Firefox', feature=feature or '<code>contain</code> and <code>cover</code>', support=support or '1.0', after_table=after_table or '') def get_default_compat_div(self): browser_id = self.version.browser_id version_id = self.version.id return { 'name': u'desktop', 'browsers': [{ 'id': browser_id, 'name': 'Firefox for Desktop', 'slug': 'firefox_desktop' }], 'versions': [{ 'browser': browser_id, 'id': version_id, 'version': '1.0' }], 'features': [{ 'id': '_contain and cover', 'name': '<code>contain</code> and <code>cover</code>', 'slug': 'web-css-background-size_contain_and_cover' }], 'supports': [{ 'feature': '_contain and cover', 'id': '__contain and cover-%s' % version_id, 'support': 'yes', 'version': version_id }] } def assert_extract(self, html, compat_divs=None, footnotes=None, issues=None, embedded=None): parsed = kumascript_grammar['html'].parse(html) out = self.visitor.visit(parsed) extractor = CompatSectionExtractor(feature=self.feature, elements=out) extracted = extractor.extract() self.assertEqual(extracted['compat_divs'], compat_divs or []) self.assertEqual(extracted['footnotes'], footnotes or {}) self.assertEqual(extracted['issues'], issues or []) self.assertEqual(extracted['embedded'], embedded or []) def test_standard(self): html = self.construct_html() expected = self.get_default_compat_div() self.assert_extract(html, [expected]) def test_unknown_browser(self): html = self.construct_html(browser='Fire') expected = self.get_default_compat_div() expected['browsers'][0] = { 'id': '_Fire', 'name': 'Fire', 'slug': '_Fire' } expected['versions'][0] = { 'id': '_Fire-1.0', 'version': '1.0', 'browser': '_Fire' } expected['supports'][0] = { 'id': u'__contain and cover-_Fire-1.0', 'support': 'yes', 'feature': '_contain and cover', 'version': '_Fire-1.0' } issue = ('unknown_browser', 205, 218, {'name': 'Fire'}) self.assert_extract(html, [expected], issues=[issue]) def test_wrong_first_column_header(self): # All known pages use "Feature" for first column, but be ready html = self.construct_html() html = html.replace('<th>Feature</th>', '<th>Features</th>') expected = self.get_default_compat_div() issue = ('feature_header', 180, 197, {'header': 'Features'}) self.assert_extract(html, [expected], issues=[issue]) def test_footnote(self): html = self.construct_html( support='1.0 [1]', after_table='<p>[1] This is a footnote.</p>') expected = self.get_default_compat_div() expected['supports'][0]['footnote'] = 'This is a footnote.' expected['supports'][0]['footnote_id'] = ('1', 322, 325) self.assert_extract(html, [expected]) def test_footnote_mismatch(self): html = self.construct_html( support='1.0 [1]', after_table='<p>[2] Oops, footnote ID is wrong.</p>') expected = self.get_default_compat_div() expected['supports'][0]['footnote_id'] = ('1', 322, 325) footnotes = {'2': ('Oops, footnote ID is wrong.', 374, 412)} issues = [('footnote_missing', 322, 325, { 'footnote_id': '1' }), ('footnote_unused', 374, 412, { 'footnote_id': '2' })] self.assert_extract(html, [expected], footnotes=footnotes, issues=issues) def test_extra_row_cell(self): # https://developer.mozilla.org/en-US/docs/Web/JavaScript/ # Reference/Global_Objects/WeakSet, March 2015 html = self.construct_html() html = html.replace('<td>1.0</td>', '<td>1.0</td><td>{{CompatUnknown()}}</td>') self.assertTrue('CompatUnknown' in html) expected = self.get_default_compat_div() issue = ('extra_cell', 326, 354, {}) self.assert_extract(html, [expected], issues=[issue]) def test_compat_mobile_table(self): mobile = """ <div id="compat-mobile"> <table class="compat-table"> <tbody> <tr><th>Feature</th><th>Safari Mobile</th></tr> <tr> <td><code>contain</code> and <code>cover</code></td> <td>1.0 [1]</td> </tr> </tbody> </table> </div> <p></p> <p>[1] It's really supported.</p> """ html = self.construct_html(after_table=mobile) expected_desktop = self.get_default_compat_div() expected_mobile = { 'name': 'mobile', 'browsers': [{ 'id': '_Safari for iOS', 'name': 'Safari for iOS', 'slug': '_Safari for iOS', }], 'features': [{ 'id': '_contain and cover', 'name': '<code>contain</code> and <code>cover</code>', 'slug': 'web-css-background-size_contain_and_cover', }], 'versions': [{ 'id': '_Safari for iOS-1.0', 'version': '1.0', 'browser': '_Safari for iOS', }], 'supports': [{ 'id': '__contain and cover-_Safari for iOS-1.0', 'feature': '_contain and cover', 'support': 'yes', 'version': '_Safari for iOS-1.0', 'footnote': "It's really supported.", 'footnote_id': ('1', 581, 584), }], } issue = ('unknown_browser', 465, 487, {'name': 'Safari Mobile'}) self.assert_extract(html, [expected_desktop, expected_mobile], issues=[issue]) def test_pre_content(self): header_plus = ( '<h2 id="Browser_compatibility">Browser compatibility</h2>' '<p>Here\'s some extra content.</p>') html = self.construct_html(header=header_plus) expected = self.get_default_compat_div() issue = ('skipped_content', 57, 90, {}) self.assert_extract(html, [expected], issues=[issue]) def test_feature_issue(self): html = self.construct_html( feature='<code>contain</code> and <code>cover</code> [1]') expected = self.get_default_compat_div() issue = ('footnote_feature', 300, 304, {}) self.assert_extract(html, [expected], issues=[issue]) def test_support_issue(self): html = self.construct_html(support='1.0 (or earlier)') expected = self.get_default_compat_div() issue = ('inline_text', 322, 334, {'text': '(or earlier)'}) self.assert_extract(html, [expected], issues=[issue]) def test_footnote_issue(self): html = self.construct_html(after_table="<p>Here's some text.</p>") expected = self.get_default_compat_div() issue = ('footnote_no_id', 370, 394, {}) self.assert_extract(html, [expected], issues=[issue]) def test_table_div_wraps_h3(self): # https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode html = self.construct_html() html = html.replace('</div>', '<h3>Gecko Notes</h3><p>It rocks</p></div>') expected = self.get_default_compat_div() issues = [('skipped_content', 58, 126, {}), ('footnote_gap', 434, 438, {}), ('footnote_no_id', 418, 433, {})] self.assert_extract(html, [expected], issues=issues) def test_support_colspan_exceeds_table_width(self): # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent html = self.construct_html() html = html.replace('<td>1.0', '<td colspan="2">1.0') expected = self.get_default_compat_div() issue = ('cell_out_of_bounds', 314, 338, {}) self.assert_extract(html, [expected], issues=[issue]) def test_embedded(self): html = self.construct_html( after_table="<div>{{EmbedCompatTable('foo-bar')}}</div>") expected = self.get_default_compat_div() self.assert_extract(html, [expected], embedded=['foo-bar'])