Esempio n. 1
0
 def setUp(self):
     self.visitor = KumaVisitor()
 def setUp(self):
     self.visitor = KumaVisitor()
     self.spec = self.get_instance('Specification', 'css3_backgrounds')
Esempio n. 3
0
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)
Esempio n. 6
0
 def setUp(self):
     self.visitor = KumaVisitor()
     self.spec = self.get_instance('Specification', 'css3_backgrounds')
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'])
Esempio n. 8
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)
Esempio n. 9
0
 def setUp(self):
     self.visitor = KumaVisitor()
Esempio n. 10
0
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'})])
Esempio n. 11
0
 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'))
Esempio n. 12
0
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'])