Merge pull request #10446 from w3c/ffs-descriptor

test the @font-face font feature settings descriptor (property already tested)
diff --git a/.taskcluster.yml b/.taskcluster.yml
new file mode 100644
index 0000000..11109a5
--- /dev/null
+++ b/.taskcluster.yml
@@ -0,0 +1,941 @@
+# GENERATED FILE DO NOT EDIT
+# To regenerate this file run ./wpt make-tasks
+allowPullRequests: collaborators
+tasks:
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-1, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 1 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-2, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 2 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-3, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 3 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-4, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 4 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-5, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 5 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-6, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 6 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-7, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 7 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-8, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 8 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-9, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 9 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-reftest-10, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome reftest 10 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-wdspec-1, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome wdspec 1 1"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-1, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 1 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-2, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 2 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-3, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 3 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-4, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 4 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-5, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 5 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-6, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 6 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-7, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 7 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-8, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 8 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-9, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 9 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-10, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 10 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-11, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 11 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-12, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 12 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-13, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 13 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-14, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 14 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-chrome-dev-testharness-15, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ chrome testharness 15 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-1, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 1 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-2, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 2 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-3, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 3 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-4, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 4 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-5, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 5 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-6, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 6 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-7, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 7 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-8, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 8 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-9, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 9 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-reftest-10, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox reftest 10 10"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-wdspec-1, owner: '{{ event.head.user.email
+      }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox wdspec 1 1"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-1, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 1 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-2, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 2 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-3, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 3 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-4, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 4 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-5, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 5 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-6, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 6 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-7, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 7 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-8, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 8 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-9, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 9 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-10, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 10 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-11, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 11 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-12, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 12 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-13, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 13 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-14, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 14 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+- extra:
+    github:
+      branches: [master]
+      events: [push]
+  metadata: {description: '', name: wpt-firefox-nightly-testharness-15, owner: '{{
+      event.head.user.email }}', source: '{{ event.head.repo.url }}'}
+  payload:
+    artifacts:
+      public/results: {path: /home/test/artifacts, type: directory}
+    command: [/bin/bash, --login, -c, ">-\n            ~/start.sh &&\n           \
+        \ cd /home/test/web-platform-tests &&\n            git fetch {{event.head.repo.url}}\
+        \ &&\n            git config advice.detachedHead false &&\n            git\
+        \ checkout {{event.head.sha}} &&\n            ./tools/ci/ci_taskcluster.sh\
+        \ firefox testharness 15 15"]
+    image: harjgam/web-platform-tests:0.6
+    maxRunTime: 7200
+  provisionerId: '{{ taskcluster.docker.provisionerId }}'
+  workerType: '{{ taskcluster.docker.workerType }}'
+version: 0
diff --git a/2dcontext/drawing-paths-to-the-canvas/canvas_focus_drawCustomFocusRing_001.html b/2dcontext/drawing-paths-to-the-canvas/canvas_focus_drawCustomFocusRing_001.html
deleted file mode 100644
index 56d5ebb..0000000
--- a/2dcontext/drawing-paths-to-the-canvas/canvas_focus_drawCustomFocusRing_001.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>canvas drawCustomFocusRing() step1 test</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <link rel="author" title="Takeshi Kurosawa" href="mailto:kurosawa-takeshi@mitsue.co.jp">
-    <link rel="help" href="http://www.w3.org/TR/2dcontext/#dom-context-2d-drawcustomfocusring">
-  </head>
-  <body>
-    <h1>Description</h1>
-    <p>This test checks whether drawCustomFocusRing returns false if the element passed as an argument is not focused or is not a descendant of the element with whose context the method is associated.</p>
-    <div id="log"></div>
-    <div>
-      <input type="text" id="text0">
-      <canvas id="canvas"><input type="text" id="text1"></canvas>
-    </div>
-    <script>
-    (function() {
-      test(function() {
-          var canvas = document.getElementById('canvas');
-          var context = canvas.getContext('2d');
-          var text0 = document.getElementById('text0');
-          text0.focus(); // document.activeElement === text0;
-
-          var text1 = document.getElementById('text1');
-          assert_false(context.drawCustomFocusRing(text1));
-      }, 'drawCustomFocusRing must return false for an element that is not focused.');
-
-      test(function() {
-          var canvas = document.getElementById('canvas');
-          var context = canvas.getContext('2d');
-          var text0 = document.getElementById('text0');
-          text0.focus(); // document.activeElement === text0;
-
-          assert_false(context.drawCustomFocusRing(text0));
-      }, 'drawCustomFocusRing must return false for an element that is not a descendant of the canvas element.');
-    })();
-    </script>
-  </body>
-</html>
diff --git a/2dcontext/imagebitmap/common.sub.js b/2dcontext/imagebitmap/common.sub.js
index d4d8419..7f99396 100644
--- a/2dcontext/imagebitmap/common.sub.js
+++ b/2dcontext/imagebitmap/common.sub.js
@@ -32,35 +32,43 @@
     });
 }
 
+var imageBitmapVideoPromise = new Promise(function(resolve, reject) {
+    var video = document.createElement("video");
+    video.oncanplaythrough = function() {
+        resolve(video);
+    };
+    video.onerror = reject;
+    video.src = getVideoURI("/images/pattern");
+
+    // Prevent WebKit from garbage collecting event handlers.
+    window._video = video;
+});
+
 function makeVideo() {
-    return new Promise(function(resolve, reject) {
-        var video = document.createElement("video");
-        video.oncanplaythrough = function() {
-            resolve(video);
-        };
-        video.onerror = reject;
-        video.src = getVideoURI("/images/pattern");
-    });
+    return imageBitmapVideoPromise;
 }
 
-function makeDataUrlVideo() {
-    const toDataUrl = (type, buffer) => {
-        const encoded = btoa(String.fromCodePoint(...new Uint8Array(buffer)));
-        return `data:${type};base64,${encoded}`
-    };
+var imageBitmapDataUrlVideoPromise = fetch(getVideoURI("/images/pattern"))
+    .then(response => Promise.all([response.headers.get("Content-Type"), response.arrayBuffer()]))
+    .then(([type, data]) => {
+        return new Promise(function(resolve, reject) {
+            var video = document.createElement("video");
+            video.oncanplaythrough = function() {
+                resolve(video);
+            };
+            video.onerror = reject;
 
-    return fetch(getVideoURI("/images/pattern"))
-        .then(response => Promise.all([response.headers.get("Content-Type"), response.arrayBuffer()]))
-        .then(([type, data]) => {
-            return new Promise(function(resolve, reject) {
-                var video = document.createElement("video");
-                video.oncanplaythrough = function() {
-                    resolve(video);
-                };
-                video.onerror = reject;
-                video.src = toDataUrl(type, data);
-            });
+            var encoded = btoa(String.fromCodePoint(...new Uint8Array(data)));
+            var dataUrl = `data:${type};base64,${encoded}`;
+            video.src = dataUrl;
+
+            // Prevent WebKit from garbage collecting event handlers.
+            window._dataVideo = video;
         });
+    });
+
+function makeDataUrlVideo() {
+    return imageBitmapDataUrlVideoPromise;
 }
 
 function makeMakeHTMLImage(src) {
diff --git a/FileAPI/historical.https.html b/FileAPI/historical.https.html
index 9d78b56..877aed0 100644
--- a/FileAPI/historical.https.html
+++ b/FileAPI/historical.https.html
@@ -10,17 +10,20 @@
  <body>
   <div id="log"></div>
   <script>
-    test(function() {
-        assert_false('toNativeLineEndings' in window);
-    }, '"toNativeLineEndings" should not be supported');
+    var removedFromWindow = [
+        'toNativeLineEndings',
+        'FileError',
+        'FileException',
+        'FileHandle',
+        'FileRequest',
+        'MutableFile',
+    ];
 
-    test(function() {
-        assert_false('FileError' in window);
-    }, '"FileError" should not be supported');
-
-    test(function() {
-        assert_false('FileException' in window);
-    }, '"FileException" should not be supported');
+    removedFromWindow.forEach(function(name) {
+        test(function() {
+            assert_false(name in window);
+        }, '"' + name + '" should not be supported');
+    });
 
     test(function() {
         var b = new Blob();
diff --git a/FileAPI/reading-data-section/FileReader-multiple-reads.html b/FileAPI/reading-data-section/FileReader-multiple-reads.html
index ca04f3c..310fa85 100644
--- a/FileAPI/reading-data-section/FileReader-multiple-reads.html
+++ b/FileAPI/reading-data-section/FileReader-multiple-reads.html
@@ -70,4 +70,20 @@
   reader.readAsArrayBuffer(blob_1)
   assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
 }, 'test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer');
+
+async_test(function() {
+  var blob_1 = new Blob([new Uint8Array(0x414141)]);
+  var blob_2 = new Blob(['TEST000000002']);
+  var reader = new FileReader();
+  reader.onloadstart = this.step_func(function() {
+    reader.abort();
+    reader.onloadstart = null;
+    reader.onloadend = this.step_func_done(function() {
+      assert_equals('TEST000000002', reader.result);
+    });
+    reader.readAsText(blob_2);
+  });
+  reader.readAsText(blob_1);
+}, 'test abort and restart in onloadstart event for readAsText');
+
 </script>
diff --git a/IndexedDB/historical.html b/IndexedDB/historical.html
index 73d78c8..8e7097e 100644
--- a/IndexedDB/historical.html
+++ b/IndexedDB/historical.html
@@ -63,4 +63,18 @@
   // Replaced circa May 2012 by a DOMString (later, IDBTransactionMode enum).
   assert_false('VERSION_CHANGE' in IDBTransaction);
 }, '"VERSION_CHANGE" should not be supported on IDBTransaction.');
+
+// Gecko-proprietary interfaces.
+var removedFromWindow = [
+  'IDBFileHandle',
+  'IDBFileRequest',
+  'IDBMutableFile',
+];
+
+removedFromWindow.forEach(function(name) {
+  test(function() {
+    assert_false(name in window);
+  }, '"' + name + '" should not be supported');
+});
+
 </script>
diff --git a/README.md b/README.md
index 516af89..182e4e7 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@
 **On Windows**: You will need to preceed the prior command with
 `python` or the path to the python binary.
 ```bash
-python wpt product [tests]
+python wpt run product [tests]
 ```
 
 where `product` is currently `firefox` or `chrome` and `[tests]` is a
@@ -410,6 +410,14 @@
 pushed here without any further review by supplying a link to the
 upstream review.
 
+Search filters to find things to review:
+
+* [Open PRs (excluding vendor exports)](https://github.com/w3c/web-platform-tests/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+-label%3A%22mozilla%3Agecko-sync%22+-label%3A%22chromium-export%22+-label%3A%22webkit-export%22+-label%3A%22servo-export%22)
+* [Reviewed but still open PRs (excluding vendor exports)](https://github.com/w3c/web-platform-tests/pulls?q=is%3Apr+is%3Aopen+-label%3Amozilla%3Agecko-sync+-label%3Achromium-export+-label%3Awebkit-export+-label%3Aservo-export+review%3Aapproved) (Merge? Something left to fix? Ping other reviewer?)
+* [Open PRs without owners](https://github.com/w3c/web-platform-tests/pulls?q=is%3Apr+is%3Aopen+label%3Astatus%3Aneeds-owners)
+* [Open PRs with label `infra` (excluding vendor exports)](https://github.com/w3c/web-platform-tests/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+label%3Ainfra+-label%3A%22mozilla%3Agecko-sync%22+-label%3A%22chromium-export%22+-label%3A%22webkit-export%22+-label%3A%22servo-export%22)
+* [Open PRs with label `docs` (excluding vendor exports)](https://github.com/w3c/web-platform-tests/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+label%3Adocs+-label%3A%22mozilla%3Agecko-sync%22+-label%3A%22chromium-export%22+-label%3A%22webkit-export%22+-label%3A%22servo-export%22)
+
 Getting Involved
 ================
 
diff --git a/acid/OWNERS b/acid/OWNERS
new file mode 100644
index 0000000..fd31fb2
--- /dev/null
+++ b/acid/OWNERS
@@ -0,0 +1 @@
+@Ms2ger
diff --git a/client-hints/accept_ch_malformed_header.https.html b/client-hints/accept_ch_malformed_header.https.html
new file mode 100644
index 0000000..3de78b7
--- /dev/null
+++ b/client-hints/accept_ch_malformed_header.https.html
@@ -0,0 +1,23 @@
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+
+promise_test(t => {
+  return fetch("/client-hints/echo_client_hints_received.py").then(r => {
+    assert_equals(r.status, 200)
+    // Verify that the browser does not include client hints in the headers
+    // since Accept-CH is malformed (includes whitespace between attributes
+    // instead of comma).
+    assert_false(r.headers.has("device-memory-received"), "device-memory-received");
+    assert_false(r.headers.has("dpr-received"), "dpr-received");
+    assert_false(r.headers.has("viewport-width-received"), "viewport-width-received");
+  });
+}, "Accept-CH header test");
+
+</script>
+
+</body>
+</html>
diff --git a/client-hints/accept_ch_malformed_header.https.html.headers b/client-hints/accept_ch_malformed_header.https.html.headers
new file mode 100644
index 0000000..6d23758
--- /dev/null
+++ b/client-hints/accept_ch_malformed_header.https.html.headers
@@ -0,0 +1 @@
+Accept-CH: device-memory dpr
\ No newline at end of file
diff --git a/compat/webkit-background-origin-text-ref.html b/compat/webkit-background-origin-text-ref.html
new file mode 100644
index 0000000..d1f1838
--- /dev/null
+++ b/compat/webkit-background-origin-text-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>webkit-background-origin should not accept text as value</title>
+<link rel="author" title="Rob Buis" href="rob.buis@chromium.org">
+<p>There should be a green square below and no red.</p>
+<div style="width: 100px; height: 100px; background-color: green"></div>
diff --git a/compat/webkit-background-origin-text.html b/compat/webkit-background-origin-text.html
new file mode 100644
index 0000000..06e7283
--- /dev/null
+++ b/compat/webkit-background-origin-text.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>webkit-background-origin should not accept text as value</title>
+<link rel="author" title="Rob Buis" href="rob.buis@chromium.org">
+<link rel="match" href="webkit-background-origin-text-ref.html">
+<style>
+#target {
+  width: 0px;
+  background-image: linear-gradient(green, green 50%, red 50%, red);
+  background-size: 200px 200px;
+  background-origin: border-box;
+  -webkit-background-origin: text;
+  padding: 25px;
+  border: 25px solid transparent;
+}
+</style>
+<p>There should be a green square below and no red.</p>
+<div id="target"></div>
diff --git a/config.default.json b/config.default.json
index d08d7c4..4d1c814 100644
--- a/config.default.json
+++ b/config.default.json
@@ -1,4 +1,7 @@
 {"browser_host": "web-platform.test",
+ "alternate_hosts": {
+     "alt": "not-web-platform.test"
+ },
  "doc_root": null,
  "ws_doc_root": null,
  "server_host": null,
diff --git a/console/console-tests-historical.any.js b/console/console-tests-historical.any.js
new file mode 100644
index 0000000..4c4d4c2
--- /dev/null
+++ b/console/console-tests-historical.any.js
@@ -0,0 +1,19 @@
+/**
+ * These tests assert the non-existence of certain
+ * legacy Console methods that are not included in
+ * the specification: http://console.spec.whatwg.org/
+ */
+
+"use strict";
+
+test(() => {
+  assert_equals(console.timeline, undefined, "console.timeline should be undefined");
+}, "'timeline' function should not exist on the console object");
+
+test(() => {
+  assert_equals(console.timelineEnd, undefined, "console.timelineEnd should be undefined");
+}, "'timelineEnd' function should not exist on the console object");
+
+test(() => {
+  assert_equals(console.markTimeline, undefined, "console.markTimeline should be undefined");
+}, "'markTimeline' function should not exist on the console object");
diff --git a/console/console-timeline-timelineEnd-historical.any.js b/console/console-timeline-timelineEnd-historical.any.js
deleted file mode 100644
index 038c715..0000000
--- a/console/console-timeline-timelineEnd-historical.any.js
+++ /dev/null
@@ -1,9 +0,0 @@
-"use strict";
-
-test(() => {
-  assert_equals(console.timeline, undefined, "console.timeline should be undefined");
-}, "'timeline' function should not exist on the console object");
-
-test(() => {
-  assert_equals(console.timelineEnd, undefined, "console.timelineEnd should be undefined");
-}, "'timelineEnd' function should not exist on the console object");
\ No newline at end of file
diff --git a/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js b/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
index c48a136..127a94b 100644
--- a/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
+++ b/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
@@ -118,7 +118,7 @@
     window.onmessage = function (e) {
       if (e.source != i.contentWindow)
           return;
-      t.unreached_func('No message should be sent from the frame.');
+      t.assert_unreached('No message should be sent from the frame.');
     }
     i.onload = t.step_func(function () {
       // Delay the check until after the postMessage has a chance to execute.
@@ -138,12 +138,18 @@
       t.done();
     }));
   } else {
-    // Assert iframe loads.
+    // Assert iframe loads.  Wait for both the load event and the postMessage.
+    window.addEventListener('message', t.step_func(e => {
+      if (e.source != i.contentWindow)
+        return;
+      assert_true(loaded[urlId]);
+      if (i.onloadReceived)
+        t.done();
+    }));
     i.onload = t.step_func(function () {
-      // Delay the check until after the postMessage has a chance to execute.
-      setTimeout(t.step_func_done(function () {
-        assert_true(loaded[urlId]);
-      }), 1);
+      if (loaded[urlId])
+        t.done();
+      i.onloadReceived = true;
     });
   }
   document.body.appendChild(i);
diff --git a/content-security-policy/frame-ancestors/frame-ancestors-overrides-xfo.html b/content-security-policy/frame-ancestors/frame-ancestors-overrides-xfo.html
index 2db0b7f..4b7b099 100644
--- a/content-security-policy/frame-ancestors/frame-ancestors-overrides-xfo.html
+++ b/content-security-policy/frame-ancestors/frame-ancestors-overrides-xfo.html
@@ -10,7 +10,7 @@
             var i = document.createElement('iframe');
             i.src = "support/frame-ancestors-and-x-frame-options.sub.html?policy='self'&xfo=DENY";
             i.onload = t.step_func_done(function () {
-                assert_equals(i.contentDocument.origin, document.origin, "The same-origin page loaded.");
+                assert_equals(i.contentWindow.origin, window.origin, "The same-origin page loaded.");
             });
             document.body.appendChild(i);
         }, "A 'frame-ancestors' CSP directive overrides an 'x-frame-options' header which would block the page.");
diff --git a/content-security-policy/securitypolicyviolation/idl.html b/content-security-policy/securitypolicyviolation/idl.html
index 2853b86..1849abc 100644
--- a/content-security-policy/securitypolicyviolation/idl.html
+++ b/content-security-policy/securitypolicyviolation/idl.html
@@ -40,9 +40,9 @@
 </script>
 <script>
   promise_test(async function() {
-    let dom = await fetch('/interfaces/dom.idl').then(r => r.text());
+    const dom = await fetch('/interfaces/dom.idl').then(r => r.text());
 
-    var idl_array = new IdlArray();
+    const idl_array = new IdlArray();
     idl_array.add_untested_idls(document.querySelector('#untested').textContent);
     idl_array.add_untested_idls(dom, { only: ['Event', 'EventInit'] });
     idl_array.add_idls(document.querySelector('#tested').textContent);
diff --git a/cookie-store/cookieStore_delete_basic.tentative.window.js b/cookie-store/cookieStore_delete_basic.tentative.window.js
new file mode 100644
index 0000000..8a3f7ed
--- /dev/null
+++ b/cookie-store/cookieStore_delete_basic.tentative.window.js
@@ -0,0 +1,10 @@
+'use strict';
+
+promise_test(async testCase => {
+  const p = cookieStore.delete('cookie-name');
+  assert_true(p instanceof Promise,
+              'cookieStore.delete() returns a promise');
+  const result = await p;
+  assert_equals(result, undefined,
+                'cookieStore.delete() promise resolves to undefined');
+}, 'cookieStore.delete return type is Promise<void>');
diff --git a/cookie-store/delete_cookies.tentative.html b/cookie-store/delete_cookies.tentative.html
deleted file mode 100644
index 9fa0ee4..0000000
--- a/cookie-store/delete_cookies.tentative.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Async Cookies: delete cookies</title>
-<meta name="help" href="https://github.com/WICG/cookie-store/">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="resources/cookie-test-helpers.js"></script>
-<script src="resources/delete_cookies.js"></script>
diff --git a/cookie-store/delete_cookies.tentative.https.html b/cookie-store/delete_cookies.tentative.https.html
deleted file mode 100644
index 192f79b..0000000
--- a/cookie-store/delete_cookies.tentative.https.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Async Cookies: delete cookies (HTTPS)</title>
-<meta name="help" href="https://github.com/WICG/cookie-store/">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="resources/cookie-test-helpers.js"></script>
-<script src="resources/delete_cookies.js"></script>
diff --git a/cookie-store/get_set_get_all.tentative.html b/cookie-store/get_set_get_all.tentative.html
deleted file mode 100644
index a88d032..0000000
--- a/cookie-store/get_set_get_all.tentative.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Async Cookies: get, set, getAll</title>
-<meta name="help" href="https://github.com/WICG/cookie-store/">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="resources/cookie-test-helpers.js"></script>
-<script src="resources/get_set_get_all.js"></script>
diff --git a/cookie-store/get_set_get_all.tentative.https.html b/cookie-store/get_set_get_all.tentative.https.html
deleted file mode 100644
index 1769b16..0000000
--- a/cookie-store/get_set_get_all.tentative.https.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Async Cookies: get, set, getAll (HTTPS)</title>
-<meta name="help" href="https://github.com/WICG/cookie-store/">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="resources/cookie-test-helpers.js"></script>
-<script src="resources/get_set_get_all.js"></script>
diff --git a/cookie-store/httponly_cookies.window.js b/cookie-store/httponly_cookies.window.js
new file mode 100644
index 0000000..01239ae
--- /dev/null
+++ b/cookie-store/httponly_cookies.window.js
@@ -0,0 +1,47 @@
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+  let eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringHttp('HTTPONLY-cookie=value; path=/; httponly');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'HttpOnly cookie we wrote using HTTP in cookie jar' +
+        ' is invisible to script');
+  assert_equals(
+      await getCookieStringHttp(),
+      'HTTPONLY-cookie=value',
+    'HttpOnly cookie we wrote using HTTP in HTTP cookie jar');
+
+  await setCookieStringHttp('HTTPONLY-cookie=new-value; path=/; httponly');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'HttpOnly cookie we overwrote using HTTP in cookie jar' +
+        ' is invisible to script');
+  assert_equals(
+      await getCookieStringHttp(),
+      'HTTPONLY-cookie=new-value',
+    'HttpOnly cookie we overwrote using HTTP in HTTP cookie jar');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringHttp(
+      'HTTPONLY-cookie=DELETED; path=/; max-age=0; httponly');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+  assert_equals(
+      await getCookieStringHttp(),
+      undefined,
+      'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
+
+  // HTTPONLY cookie changes should not have been observed; perform
+  // a dummy change to verify that nothing else was queued up.
+  await cookieStore.set('TEST', 'dummy');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'TEST', value: 'dummy'}]},
+    'HttpOnly cookie deletion was not observed');
+}, 'HttpOnly cookies are not observed');
diff --git a/cookie-store/resources/cookie-test-helpers.js b/cookie-store/resources/cookie-test-helpers.js
index 20f67e3..178947a 100644
--- a/cookie-store/resources/cookie-test-helpers.js
+++ b/cookie-store/resources/cookie-test-helpers.js
@@ -11,41 +11,6 @@
 
 const kCookieHelperCgi = 'resources/cookie_helper.py';
 
-// Async wrapper for an async function or promise that is expected
-// reject in an unsecured (non-https:) context and work in a secured
-// (https:) context.
-//
-// Parameters:
-//
-// - testCase: (TestCase) test case context
-// - code: (Error class or number) expected rejection type in unsecured context
-// - promise: (thenable) test code
-// - message: (optional; string) message to forward to promise_rejects in
-//   unsecured context
-async function promise_rejects_when_unsecured(
-  testCase,
-  code,
-  promise,
-  message = 'Feature unavailable from unsecured contexts') {
-  if (kIsUnsecured)
-    await promise_rejects(testCase, code, promise, message);
-  else await promise;
-};
-
-// Converts a list of cookie records {name, value} to [name=]value; ... as
-// seen in Cookie: and document.cookie.
-//
-// Parameters:
-// - cookies: (array of {name, value}) records to convert
-//
-// Returns a string serializing the records, or undefined if no records were
-// given.
-function cookieString(cookies) {
-  return cookies.length ? cookies.map((
-    {name, value}) => (name ? (name + '=') : '') + value).join('; ') :
-  undefined;
-}
-
 // Approximate async equivalent to the document.cookie getter but with
 // important differences: optional additional getAll arguments are
 // forwarded, and an empty cookie jar returns undefined.
@@ -56,7 +21,11 @@
 // using parsed cookie jar contents and also allows expectations to be
 // written more compactly.
 async function getCookieString(...args) {
-  return cookieString(await cookieStore.getAll(...args));
+  const cookies = await cookieStore.getAll(...args);
+  return cookies.length
+    ? cookies.map(({name, value}) =>
+                  (name ? (name + '=') : '') + value).join('; ')
+    : undefined;
 }
 
 // Approximate async equivalent to the document.cookie getter but from
@@ -240,31 +209,18 @@
 async function cookie_test(func, description) {
 
   // Wipe cookies used by tests before and after the test.
-  async function deleteTestCookies() {
-    await cookieStore.delete('');
-    await cookieStore.delete('TEST');
-    await cookieStore.delete('META-🍪');
-    await cookieStore.delete('DOCUMENT-🍪');
-    await cookieStore.delete('HTTP-🍪');
-    await setCookieStringHttp(
-      'HTTPONLY-🍪=DELETED; path=/; max-age=0; httponly');
-    if (!kIsUnsecured) {
-      await cookieStore.delete('__Host-COOKIENAME');
-      await cookieStore.delete('__Host-1🍪');
-      await cookieStore.delete('__Host-2🌟');
-      await cookieStore.delete('__Host-3🌱');
-      await cookieStore.delete('__Host-unordered1🍪');
-      await cookieStore.delete('__Host-unordered2🌟');
-      await cookieStore.delete('__Host-unordered3🌱');
-    }
+  async function deleteAllCookies() {
+    (await cookieStore.getAll()).forEach(({name, value}) => {
+      cookieStore.delete(name);
+    });
   }
 
   return promise_test(async t => {
-    await deleteTestCookies();
+    await deleteAllCookies();
     try {
       return await func(t);
     } finally {
-      await deleteTestCookies();
+      await deleteAllCookies();
     }
   }, description);
 }
diff --git a/cookie-store/resources/delete_cookies.js b/cookie-store/resources/delete_cookies.js
deleted file mode 100644
index 21adadb..0000000
--- a/cookie-store/resources/delete_cookies.js
+++ /dev/null
@@ -1,44 +0,0 @@
-'use strict';
-
-cookie_test(async testCase => {
-  // TODO: This test doesn't create cookies and doesn't assert
-  // the behavior of delete(). Improve or remove it.
-
-  await cookieStore.delete('');
-  await cookieStore.delete('TEST');
-  await cookieStore.delete('META-🍪');
-  await cookieStore.delete('DOCUMENT-🍪');
-  await cookieStore.delete('HTTP-🍪');
-
-  await setCookieStringHttp(
-    'HTTPONLY-🍪=DELETED; path=/; max-age=0; httponly');
-
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-COOKIENAME'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-1🍪'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-2🌟'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-3🌱'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-unordered1🍪'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-unordered2🌟'));
-  await promise_rejects_when_unsecured(
-      testCase,
-      new TypeError(),
-      cookieStore.delete('__Host-unordered3🌱'));
-}, 'Test cookieStore.delete');
diff --git a/cookie-store/resources/document_cookie.js b/cookie-store/resources/document_cookie.js
index 1809b88..980347f 100644
--- a/cookie-store/resources/document_cookie.js
+++ b/cookie-store/resources/document_cookie.js
@@ -2,25 +2,43 @@
 
 cookie_test(async t => {
   let eventPromise = observeNextCookieChangeEvent();
-  await setCookieStringDocument('DOCUMENT-🍪=🔵; path=/');
+  await setCookieStringDocument('DOCUMENT-cookie=value; path=/');
   assert_equals(
       await getCookieString(),
-      'DOCUMENT-🍪=🔵',
+      'DOCUMENT-cookie=value',
       'Cookie we wrote using document.cookie in cookie jar');
   assert_equals(
     await getCookieStringHttp(),
-    'DOCUMENT-🍪=🔵',
+    'DOCUMENT-cookie=value',
     'Cookie we wrote using document.cookie in HTTP cookie jar');
   assert_equals(
       await getCookieStringDocument(),
-      'DOCUMENT-🍪=🔵',
+      'DOCUMENT-cookie=value',
       'Cookie we wrote using document.cookie in document.cookie');
   await verifyCookieChangeEvent(
-    eventPromise, {changed: [{name: 'DOCUMENT-🍪', value: '🔵'}]},
+    eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]},
       'Cookie we wrote using document.cookie is observed');
 
   eventPromise = observeNextCookieChangeEvent();
-  await setCookieStringDocument('DOCUMENT-🍪=DELETED; path=/; max-age=0');
+  await setCookieStringDocument('DOCUMENT-cookie=new-value; path=/');
+  assert_equals(
+      await getCookieString(),
+      'DOCUMENT-cookie=new-value',
+      'Cookie we overwrote using document.cookie in cookie jar');
+  assert_equals(
+    await getCookieStringHttp(),
+    'DOCUMENT-cookie=new-value',
+    'Cookie we overwrote using document.cookie in HTTP cookie jar');
+  assert_equals(
+      await getCookieStringDocument(),
+      'DOCUMENT-cookie=new-value',
+      'Cookie we overwrote using document.cookie in document.cookie');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]},
+      'Cookie we overwrote using document.cookie is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringDocument('DOCUMENT-cookie=DELETED; path=/; max-age=0');
   assert_equals(
       await getCookieString(),
       undefined,
@@ -37,7 +55,103 @@
       'Empty document.cookie cookie jar after document.cookie' +
         ' cookie-clearing using max-age=0');
   await verifyCookieChangeEvent(
+    eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]},
+      'Deletion observed after document.cookie cookie-clearing' +
+        ' using max-age=0');
+}, 'document.cookie set/overwrite/delete observed by CookieStore');
+
+cookie_test(async t => {
+  let eventPromise = observeNextCookieChangeEvent();
+  await cookieStore.set('DOCUMENT-cookie', 'value');
+  assert_equals(
+      await getCookieString(),
+      'DOCUMENT-cookie=value',
+      'Cookie we wrote using CookieStore in cookie jar');
+  assert_equals(
+    await getCookieStringHttp(),
+    'DOCUMENT-cookie=value',
+    'Cookie we wrote using CookieStore in HTTP cookie jar');
+  assert_equals(
+      await getCookieStringDocument(),
+      'DOCUMENT-cookie=value',
+      'Cookie we wrote using CookieStore in document.cookie');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]},
+      'Cookie we wrote using CookieStore is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await cookieStore.set('DOCUMENT-cookie', 'new-value');
+  assert_equals(
+      await getCookieString(),
+      'DOCUMENT-cookie=new-value',
+      'Cookie we overwrote using CookieStore in cookie jar');
+  assert_equals(
+    await getCookieStringHttp(),
+    'DOCUMENT-cookie=new-value',
+    'Cookie we overwrote using CookieStore in HTTP cookie jar');
+  assert_equals(
+      await getCookieStringDocument(),
+      'DOCUMENT-cookie=new-value',
+      'Cookie we overwrote using CookieStore in document.cookie');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]},
+      'Cookie we overwrote using CookieStore is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await cookieStore.delete('DOCUMENT-cookie');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'Empty cookie jar after CookieStore delete');
+  assert_equals(
+    await getCookieStringHttp(),
+    undefined,
+    'Empty HTTP cookie jar after CookieStore delete');
+  assert_equals(
+      await getCookieStringDocument(),
+      undefined,
+      'Empty document.cookie cookie jar after CookieStore delete');
+  await verifyCookieChangeEvent(
+    eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]},
+      'Deletion observed after CookieStore delete');
+}, 'CookieStore set/overwrite/delete observed by document.cookie');
+
+
+cookie_test(async t => {
+  let eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringDocument('DOCUMENT-🍪=🔵; path=/');
+  assert_equals(
+      await getCookieString(),
+      'DOCUMENT-🍪=🔵',
+      'Cookie we wrote using document.cookie in cookie jar');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'DOCUMENT-🍪', value: '🔵'}]},
+      'Cookie we wrote using document.cookie is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringDocument('DOCUMENT-🍪=DELETED; path=/; max-age=0');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'Empty cookie jar after document.cookie' +
+        ' cookie-clearing using max-age=0');
+  await verifyCookieChangeEvent(
     eventPromise, {deleted: [{name: 'DOCUMENT-🍪'}]},
       'Deletion observed after document.cookie cookie-clearing' +
         ' using max-age=0');
-}, 'Verify interoperability of document.cookie with other APIs.');
+}, 'CookieStore agrees with document.cookie on encoding non-ASCII cookies');
+
+
+cookie_test(async t => {
+  await cookieStore.set('DOCUMENT-🍪', '🔵');
+  assert_equals(
+      await getCookieStringDocument(),
+      'DOCUMENT-🍪=🔵',
+      'Cookie we wrote using CookieStore in document.cookie');
+
+  await cookieStore.delete('DOCUMENT-🍪');
+  assert_equals(
+      await getCookieStringDocument(),
+      undefined,
+      'Empty cookie jar after CookieStore delete');
+}, 'document.cookie agrees with CookieStore on encoding non-ASCII cookies');
diff --git a/cookie-store/resources/get_set_get_all.js b/cookie-store/resources/get_set_get_all.js
deleted file mode 100644
index 7c9a094..0000000
--- a/cookie-store/resources/get_set_get_all.js
+++ /dev/null
@@ -1,74 +0,0 @@
-'use strict';
-
-cookie_test(async t => {
-  let eventPromise = observeNextCookieChangeEvent();
-  await cookieStore.set('TEST', 'value0');
-  assert_equals(
-      await getCookieString(),
-      'TEST=value0',
-      'Cookie jar contains only cookie we set');
-  assert_equals(
-    await getCookieStringHttp(),
-    'TEST=value0',
-    'HTTP cookie jar contains only cookie we set');
-  await verifyCookieChangeEvent(
-    eventPromise,
-    {changed: [{name: 'TEST', value: 'value0'}]},
-    'Observed value that was set');
-
-  eventPromise = observeNextCookieChangeEvent();
-  await cookieStore.set('TEST', 'value');
-  assert_equals(
-      await getCookieString(),
-      'TEST=value',
-      'Cookie jar contains only cookie we overwrote');
-  await verifyCookieChangeEvent(
-    eventPromise,
-    {changed: [{name: 'TEST', value: 'value'}]},
-    'Observed value that was overwritten');
-
-  let allCookies = await cookieStore.getAll();
-  assert_equals(
-      allCookies[0].name,
-      'TEST',
-      'First entry in allCookies should be named TEST');
-  assert_equals(
-      allCookies[0].value,
-      'value',
-      'First entry in allCookies should have value "value"');
-  assert_equals(
-      allCookies.length,
-      1,
-      'Only one cookie should exist in allCookies');
-  let firstCookie = await cookieStore.get();
-  assert_equals(
-      firstCookie.name,
-      'TEST',
-      'First cookie should be named TEST');
-  assert_equals(
-      firstCookie.value,
-      'value',
-      'First cookie should have value "value"');
-  let allCookies_TEST = await cookieStore.getAll('TEST');
-  assert_equals(
-      allCookies_TEST[0].name,
-      'TEST',
-      'First entry in allCookies_TEST should be named TEST');
-  assert_equals(
-      allCookies_TEST[0].value,
-      'value',
-      'First entry in allCookies_TEST should have value "value"');
-  assert_equals(
-      allCookies_TEST.length,
-      1,
-      'Only one cookie should exist in allCookies_TEST');
-  let firstCookie_TEST = await cookieStore.get('TEST');
-  assert_equals(
-      firstCookie_TEST.name,
-      'TEST',
-      'First TEST cookie should be named TEST');
-  assert_equals(
-      firstCookie_TEST.value,
-      'value',
-      'First TEST cookie should have value "value"');
-}, 'Get/set/get all cookies in store');
diff --git a/cookie-store/resources/http_cookie_and_set_cookie_headers.js b/cookie-store/resources/http_cookie_and_set_cookie_headers.js
index 73534c7..0fcf6b6 100644
--- a/cookie-store/resources/http_cookie_and_set_cookie_headers.js
+++ b/cookie-store/resources/http_cookie_and_set_cookie_headers.js
@@ -2,15 +2,56 @@
 
 cookie_test(async t => {
   let eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringHttp('HTTP-cookie=value; path=/');
+  assert_equals(
+      await getCookieString(),
+      'HTTP-cookie=value',
+      'Cookie we wrote using HTTP in cookie jar');
+  assert_equals(
+      await getCookieStringHttp(),
+      'HTTP-cookie=value',
+      'Cookie we wrote using HTTP in HTTP cookie jar');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value'}]},
+    'Cookie we wrote using HTTP is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringHttp('HTTP-cookie=new-value; path=/');
+  assert_equals(
+      await getCookieString(),
+      'HTTP-cookie=new-value',
+      'Cookie we overwrote using HTTP in cookie jar');
+  assert_equals(
+      await getCookieStringHttp(),
+      'HTTP-cookie=new-value',
+      'Cookie we overwrote using HTTP in HTTP cookie jar');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value'}]},
+    'Cookie we overwrote using HTTP is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieStringHttp('HTTP-cookie=DELETED; path=/; max-age=0');
+  assert_equals(
+      await getCookieString(),
+      undefined,
+      'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+  assert_equals(
+      await getCookieStringHttp(),
+      undefined,
+      'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
+  await verifyCookieChangeEvent(
+    eventPromise, {deleted: [{name: 'HTTP-cookie'}]},
+    'Deletion observed after HTTP cookie-clearing using max-age=0');
+}, 'HTTP set/overwrite/delete observed in CookieStore');
+
+
+cookie_test(async t => {
+  let eventPromise = observeNextCookieChangeEvent();
   await setCookieStringHttp('HTTP-🍪=🔵; path=/');
   assert_equals(
       await getCookieString(),
       'HTTP-🍪=🔵',
       'Cookie we wrote using HTTP in cookie jar');
-  assert_equals(
-      await getCookieStringHttp(),
-      'HTTP-🍪=🔵',
-      'Cookie we wrote using HTTP in HTTP cookie jar');
   await verifyCookieChangeEvent(
     eventPromise, {changed: [{name: 'HTTP-🍪', value: '🔵'}]},
     'Cookie we wrote using HTTP is observed');
@@ -20,78 +61,128 @@
   assert_equals(
       await getCookieString(),
       undefined,
-      'Empty cookie jar after HTTP cookie-clearing using max-age=0');
-  assert_equals(
-      await getCookieStringHttp(),
-      undefined,
-      'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
+    'Empty cookie jar after HTTP cookie-clearing using max-age=0');
   await verifyCookieChangeEvent(
     eventPromise, {deleted: [{name: 'HTTP-🍪'}]},
     'Deletion observed after HTTP cookie-clearing using max-age=0');
-  await cookieStore.delete('HTTP-🍪');
-}, 'Interoperability of HTTP Set-Cookie: with other APIs');
+
+}, 'CookieStore agreed with HTTP headers agree on encoding non-ASCII cookies');
+
 
 cookie_test(async t => {
   let eventPromise = observeNextCookieChangeEvent();
-  await setCookieStringHttp('HTTPONLY-🍪=🔵; path=/; httponly');
+  await cookieStore.set('TEST', 'value0');
   assert_equals(
-      await getCookieString(),
-      undefined,
-      'HttpOnly cookie we wrote using HTTP in cookie jar' +
-        ' is invisible to script');
+    await getCookieString(),
+    'TEST=value0',
+    'Cookie jar contains only cookie we set');
   assert_equals(
-      await getCookieStringHttp(),
-      'HTTPONLY-🍪=🔵',
-    'HttpOnly cookie we wrote using HTTP in HTTP cookie jar');
+    await getCookieStringHttp(),
+    'TEST=value0',
+    'HTTP cookie jar contains only cookie we set');
+  await verifyCookieChangeEvent(
+    eventPromise,
+    {changed: [{name: 'TEST', value: 'value0'}]},
+    'Observed value that was set');
 
   eventPromise = observeNextCookieChangeEvent();
-  await setCookieStringHttp(
-      'HTTPONLY-🍪=DELETED; path=/; max-age=0; httponly');
+  await cookieStore.set('TEST', 'value');
   assert_equals(
-      await getCookieString(),
-      undefined,
-      'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+    await getCookieString(),
+    'TEST=value',
+    'Cookie jar contains only cookie we set');
   assert_equals(
-      await getCookieStringHttp(),
-      undefined,
-      'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
-
-  // HTTPONLY cookie changes should not have been observed; perform
-  // a dummy change to verify that nothing else was queued up.
-  await cookieStore.set('TEST', 'dummy');
+    await getCookieStringHttp(),
+    'TEST=value',
+    'HTTP cookie jar contains only cookie we set');
   await verifyCookieChangeEvent(
-    eventPromise, {changed: [{name: 'TEST', value: 'dummy'}]},
-    'HttpOnly cookie deletion was not observed');
-}, 'HttpOnly cookies are not observed');
+    eventPromise,
+    {changed: [{name: 'TEST', value: 'value'}]},
+    'Observed value that was overwritten');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await cookieStore.delete('TEST');
+  assert_equals(
+    await getCookieString(),
+    undefined,
+    'Cookie jar does not contain cookie we deleted');
+  assert_equals(
+    await getCookieStringHttp(),
+    undefined,
+    'HTTP cookie jar does not contain cookie we deleted');
+  await verifyCookieChangeEvent(
+    eventPromise,
+    {deleted: [{name: 'TEST'}]},
+    'Observed cookie that was deleted');
+}, 'CookieStore set/overwrite/delete observed in HTTP headers');
+
+
+cookie_test(async t => {
+  await cookieStore.set('🍪', '🔵');
+  assert_equals(
+    await getCookieStringHttp(),
+    '🍪=🔵',
+    'HTTP cookie jar contains only cookie we set');
+
+  await cookieStore.delete('🍪');
+  assert_equals(
+    await getCookieStringHttp(),
+    undefined,
+    'HTTP cookie jar does not contain cookie we deleted');
+}, 'HTTP headers agreed with CookieStore on encoding non-ASCII cookies');
+
 
 cookie_test(async t => {
   // Non-UTF-8 byte sequences cause the Set-Cookie to be dropped.
   let eventPromise = observeNextCookieChangeEvent();
   await setCookieBinaryHttp(
-      unescape(encodeURIComponent('HTTP-🍪=🔵')) + '\xef\xbf\xbd; path=/');
+      unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd; path=/');
   assert_equals(
       await getCookieString(),
-      'HTTP-🍪=🔵\ufffd',
+      'HTTP-cookie=value\ufffd',
       'Binary cookie we wrote using HTTP in cookie jar');
   assert_equals(
       await getCookieStringHttp(),
-      'HTTP-🍪=🔵\ufffd',
+      'HTTP-cookie=value\ufffd',
       'Binary cookie we wrote using HTTP in HTTP cookie jar');
   assert_equals(
       decodeURIComponent(escape(await getCookieBinaryHttp())),
-      'HTTP-🍪=🔵\ufffd',
+      'HTTP-cookie=value\ufffd',
       'Binary cookie we wrote in binary HTTP cookie jar');
   assert_equals(
       await getCookieBinaryHttp(),
-      unescape(encodeURIComponent('HTTP-🍪=🔵')) + '\xef\xbf\xbd',
+      unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd',
       'Binary cookie we wrote in binary HTTP cookie jar');
   await verifyCookieChangeEvent(
-    eventPromise, {changed: [{name: 'HTTP-🍪', value: '🔵\ufffd'}]},
+    eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value\ufffd'}]},
     'Binary cookie we wrote using HTTP is observed');
 
   eventPromise = observeNextCookieChangeEvent();
   await setCookieBinaryHttp(
-      unescape(encodeURIComponent('HTTP-🍪=DELETED; path=/; max-age=0')));
+      unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd; path=/');
+  assert_equals(
+      await getCookieString(),
+      'HTTP-cookie=new-value\ufffd',
+      'Binary cookie we overwrote using HTTP in cookie jar');
+  assert_equals(
+      await getCookieStringHttp(),
+      'HTTP-cookie=new-value\ufffd',
+      'Binary cookie we overwrote using HTTP in HTTP cookie jar');
+  assert_equals(
+      decodeURIComponent(escape(await getCookieBinaryHttp())),
+      'HTTP-cookie=new-value\ufffd',
+      'Binary cookie we overwrote in binary HTTP cookie jar');
+  assert_equals(
+      await getCookieBinaryHttp(),
+      unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd',
+      'Binary cookie we overwrote in binary HTTP cookie jar');
+  await verifyCookieChangeEvent(
+    eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value\ufffd'}]},
+    'Binary cookie we overwrote using HTTP is observed');
+
+  eventPromise = observeNextCookieChangeEvent();
+  await setCookieBinaryHttp(
+      unescape(encodeURIComponent('HTTP-cookie=DELETED; path=/; max-age=0')));
   assert_equals(
       await getCookieString(),
       undefined,
@@ -107,6 +198,6 @@
       'Empty binary HTTP cookie jar after' +
         ' binary HTTP cookie-clearing using max-age=0');
   await verifyCookieChangeEvent(
-    eventPromise, {deleted: [{name: 'HTTP-🍪'}]},
+    eventPromise, {deleted: [{name: 'HTTP-cookie'}]},
     'Deletion observed after binary HTTP cookie-clearing using max-age=0');
-}, 'Binary HTTP cookies');
+}, 'Binary HTTP set/overwrite/delete observed in CookieStore');
diff --git a/cookie-store/resources/ordering.js b/cookie-store/resources/ordering.js
index 884361b..86babd9 100644
--- a/cookie-store/resources/ordering.js
+++ b/cookie-store/resources/ordering.js
@@ -1,40 +1,38 @@
 'use strict';
 
-// TODO(jsbell): Does this test really need simple origin ('__Host-') cookies?
-
 cookie_test(async t => {
-  await cookieStore.set('__Host-1🍪', '🔵cookie-value1🔴');
-  await cookieStore.set('__Host-2🌟', '🌠cookie-value2🌠');
-  await cookieStore.set('__Host-3🌱', '🔶cookie-value3🔷');
+  await cookieStore.set('ordered-1', 'cookie-value1');
+  await cookieStore.set('ordered-2', 'cookie-value2');
+  await cookieStore.set('ordered-3', 'cookie-value3');
   // NOTE: this assumes no concurrent writes from elsewhere; it also
   // uses three separate cookie jar read operations where a single getAll
   // would be more efficient, but this way the CookieStore does the filtering
   // for us.
-  const matchingValues = await Promise.all([ '1🍪', '2🌟', '3🌱' ].map(
-      async suffix => (await cookieStore.get('__Host-' + suffix)).value));
+  const matchingValues = await Promise.all(['1', '2', '3'].map(
+      async suffix => (await cookieStore.get('ordered-' + suffix)).value));
   const actual = matchingValues.join(';');
-  const expected = '🔵cookie-value1🔴;🌠cookie-value2🌠;🔶cookie-value3🔷';
+  const expected = 'cookie-value1;cookie-value2;cookie-value3';
   assert_equals(actual, expected);
 }, 'Set three simple origin session cookies sequentially and ensure ' +
             'they all end up in the cookie jar in order.');
 
 cookie_test(async t => {
   await Promise.all([
-    cookieStore.set('__Host-unordered1🍪', '🔵unordered-cookie-value1🔴'),
-    cookieStore.set('__Host-unordered2🌟', '🌠unordered-cookie-value2🌠'),
-    cookieStore.set('__Host-unordered3🌱', '🔶unordered-cookie-value3🔷')
+    cookieStore.set('ordered-unordered1', 'unordered-cookie-value1'),
+    cookieStore.set('ordered-unordered2', 'unordered-cookie-value2'),
+    cookieStore.set('ordered-unordered3', 'unordered-cookie-value3')
   ]);
   // NOTE: this assumes no concurrent writes from elsewhere; it also
   // uses three separate cookie jar read operations where a single getAll
   // would be more efficient, but this way the CookieStore does the filtering
   // for us and we do not need to sort.
-  const matchingCookies = await Promise.all([ '1🍪', '2🌟', '3🌱' ].map(
-    suffix => cookieStore.get('__Host-unordered' + suffix)));
+  const matchingCookies = await Promise.all(['1', '2', '3'].map(
+    suffix => cookieStore.get('ordered-unordered' + suffix)));
   const actual = matchingCookies.map(({ value }) => value).join(';');
   const expected =
-      '🔵unordered-cookie-value1🔴;' +
-      '🌠unordered-cookie-value2🌠;' +
-      '🔶unordered-cookie-value3🔷';
+      'unordered-cookie-value1;' +
+      'unordered-cookie-value2;' +
+      'unordered-cookie-value3';
   assert_equals(actual, expected);
 }, 'Set three simple origin session cookies in undefined order using ' +
             'Promise.all and ensure they all end up in the cookie jar in any ' +
diff --git a/cookies/http-state/attribute-tests.html b/cookies/http-state/attribute-tests.html
new file mode 100644
index 0000000..6e7d5fe
--- /dev/null
+++ b/cookies/http-state/attribute-tests.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset=utf-8>
+    <title>Tests cookie attribute functionality</title>
+    <meta name=help href="https://tools.ietf.org/html/rfc6265#page-8">
+
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="resources/cookie-http-state-template.js"></script>
+  </head>
+  <body>
+    <div id="log"></div>
+    <script>
+      setup({ explicit_timeout: true });
+
+      const TEST_CASES = [
+        {file: "attribute0001", name: "Ignore cookie for Secure attribute."},
+        {file: "attribute0002", name: "Ignore cookie for seCURe attribute."},
+        {file: "attribute0003", name: "Set cookie for \"Secure\" attribute."},
+        {file: "attribute0004", name: "Ignore cookie for for Secure= attribute."},
+        {file: "attribute0005", name: "Ignore cookie for Secure=aaaa"},
+        {file: "attribute0006", name: "Set cookie for Secure qux"},
+        {file: "attribute0007", name: "Ignore cookie for Secure space equals."},
+        {file: "attribute0008", name: "Ignore cookie for Secure equals space"},
+        {file: "attribute0009", name: "Ignore cookie for Secure separated."},
+        {file: "attribute0010", name: "Ignore cookie for Secure separated v2."},
+        {file: "attribute0011", name: "Ignore cookie for Secure separated v2."},
+        {file: "attribute0012", name: "Ignore cookie for spaced Secure"},
+        {file: "attribute0013", name: "Ignore cookie for space Secure with ;."},
+        {file: "attribute0014", name: "Set cookie for Path."},
+        {file: "attribute0015", name: "Set cookie for Path=."},
+        {file: "attribute0016", name: "Set cookie for Path=/."},
+        {file: "attribute0017", name: "Ignore cookie for invalid path."},
+        {file: "attribute0018", name: "Ignore cookie for spaced invalid path."},
+        {file: "attribute0019", name: "Ignore cookie for spaced invalid path v2."},
+        {file: "attribute0020", name: "Ignore cookie for invalid path and attribute."},
+        {file: "attribute0021", name: "Ignore cookie for invalid and root path."},
+        {file: "attribute0022", name: "Set cookie for root and invalid path."},
+        {file: "attribute0023", name: "Set cookie for invalid and sane path."},
+        {file: "attribute0024", name: "Ignore cookie for sane and invalid path."},
+        {file: "attribute0025", name: "Ignore cookie for invalid + Secure."},
+        {file: "attribute0026", name: "Ignore cookie for quoted invalid attribute."},
+      ];
+
+      for (const i in TEST_CASES) {
+        const t = TEST_CASES[i];
+        promise_test(createCookieTest(t.file),
+                     t.file + " - " + t.name,
+                     { timeout: 3000 });
+      }
+
+    </script>
+  </body>
+</html>
diff --git a/cookies/http-state/all-tests.html b/cookies/http-state/general-tests.html
similarity index 100%
rename from cookies/http-state/all-tests.html
rename to cookies/http-state/general-tests.html
diff --git a/cookies/http-state/resources/cookie-http-state-template.js b/cookies/http-state/resources/cookie-http-state-template.js
index 68c5212..470d46e 100644
--- a/cookies/http-state/resources/cookie-http-state-template.js
+++ b/cookies/http-state/resources/cookie-http-state-template.js
@@ -6,11 +6,16 @@
 }
 
 function getLocalResourcesPath() {
-  return location.pathname.replace(/[^\/]*$/, "") + SERVER_LOCATION;
+  let replace = "(" + SERVER_LOCATION + "\/)*";  // Redundant location.
+  replace += "[^\/]*$";  // Everything after the last "/".
+  return location.pathname.replace(new RegExp(replace), "") + SERVER_LOCATION;
 }
 
 function getAbsoluteServerLocation() {
-  return getLocalResourcesPath().replace(/resources.*$/,'')+ SERVER_SCRIPT;
+  // Replace the server location and everything coming after it ...
+  let replace = SERVER_LOCATION + ".*$";
+  // ... with the Server script (which includes the server location).
+  return getLocalResourcesPath().replace(new RegExp(replace),'')+ SERVER_SCRIPT;
 }
 
 function expireCookie(name, expiry_date, path) {
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0001-expected
similarity index 100%
rename from html/browsers/origin/.gitkeep
rename to cookies/http-state/resources/test-files/attribute0001-expected
diff --git a/cookies/http-state/resources/test-files/attribute0001-test b/cookies/http-state/resources/test-files/attribute0001-test
new file mode 100644
index 0000000..6199f78
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0001-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0002-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0002-expected
diff --git a/cookies/http-state/resources/test-files/attribute0002-test b/cookies/http-state/resources/test-files/attribute0002-test
new file mode 100644
index 0000000..047a24d
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0002-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; seCURe
diff --git a/cookies/http-state/resources/test-files/attribute0003-expected b/cookies/http-state/resources/test-files/attribute0003-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0003-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0003-test b/cookies/http-state/resources/test-files/attribute0003-test
new file mode 100644
index 0000000..c944bac
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0003-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; "Secure"
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0004-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0004-expected
diff --git a/cookies/http-state/resources/test-files/attribute0004-test b/cookies/http-state/resources/test-files/attribute0004-test
new file mode 100644
index 0000000..bcfaa7d8
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0004-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure=
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0005-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0005-expected
diff --git a/cookies/http-state/resources/test-files/attribute0005-test b/cookies/http-state/resources/test-files/attribute0005-test
new file mode 100644
index 0000000..1671087
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0005-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure=aaaa
diff --git a/cookies/http-state/resources/test-files/attribute0006-expected b/cookies/http-state/resources/test-files/attribute0006-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0006-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0006-test b/cookies/http-state/resources/test-files/attribute0006-test
new file mode 100644
index 0000000..39d7589
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0006-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0007-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0007-expected
diff --git a/cookies/http-state/resources/test-files/attribute0007-test b/cookies/http-state/resources/test-files/attribute0007-test
new file mode 100644
index 0000000..f75f46a
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0007-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure =aaaaa
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0008-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0008-expected
diff --git a/cookies/http-state/resources/test-files/attribute0008-test b/cookies/http-state/resources/test-files/attribute0008-test
new file mode 100644
index 0000000..be45b3a
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0008-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure= aaaaa
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0009-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0009-expected
diff --git a/cookies/http-state/resources/test-files/attribute0009-test b/cookies/http-state/resources/test-files/attribute0009-test
new file mode 100644
index 0000000..1a44c22
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0009-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure; qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0010-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0010-expected
diff --git a/cookies/http-state/resources/test-files/attribute0010-test b/cookies/http-state/resources/test-files/attribute0010-test
new file mode 100644
index 0000000..ca62200
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0010-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure;qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0011-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0011-expected
diff --git a/cookies/http-state/resources/test-files/attribute0011-test b/cookies/http-state/resources/test-files/attribute0011-test
new file mode 100644
index 0000000..3bd4c2e
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0011-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Secure    ; qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0012-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0012-expected
diff --git a/cookies/http-state/resources/test-files/attribute0012-test b/cookies/http-state/resources/test-files/attribute0012-test
new file mode 100644
index 0000000..6e7b816
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0012-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar;                Secure
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0013-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0013-expected
diff --git a/cookies/http-state/resources/test-files/attribute0013-test b/cookies/http-state/resources/test-files/attribute0013-test
new file mode 100644
index 0000000..f4814e4
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0013-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar;       Secure     ;
diff --git a/cookies/http-state/resources/test-files/attribute0014-expected b/cookies/http-state/resources/test-files/attribute0014-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0014-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0014-test b/cookies/http-state/resources/test-files/attribute0014-test
new file mode 100644
index 0000000..ef88896
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0014-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path
diff --git a/cookies/http-state/resources/test-files/attribute0015-expected b/cookies/http-state/resources/test-files/attribute0015-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0015-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0015-test b/cookies/http-state/resources/test-files/attribute0015-test
new file mode 100644
index 0000000..cea7060
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0015-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=
diff --git a/cookies/http-state/resources/test-files/attribute0016-expected b/cookies/http-state/resources/test-files/attribute0016-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0016-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0016-test b/cookies/http-state/resources/test-files/attribute0016-test
new file mode 100644
index 0000000..9a5b591
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0016-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0017-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0017-expected
diff --git a/cookies/http-state/resources/test-files/attribute0017-test b/cookies/http-state/resources/test-files/attribute0017-test
new file mode 100644
index 0000000..a6aeeb3
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0017-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0018-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0018-expected
diff --git a/cookies/http-state/resources/test-files/attribute0018-test b/cookies/http-state/resources/test-files/attribute0018-test
new file mode 100644
index 0000000..f912201
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0018-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path    =/qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0019-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0019-expected
diff --git a/cookies/http-state/resources/test-files/attribute0019-test b/cookies/http-state/resources/test-files/attribute0019-test
new file mode 100644
index 0000000..a424c6e
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0019-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=    /qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0020-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0020-expected
diff --git a/cookies/http-state/resources/test-files/attribute0020-test b/cookies/http-state/resources/test-files/attribute0020-test
new file mode 100644
index 0000000..367d2a1
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0020-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/qux      ; taz
diff --git a/cookies/http-state/resources/test-files/attribute0021-expected b/cookies/http-state/resources/test-files/attribute0021-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0021-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0021-test b/cookies/http-state/resources/test-files/attribute0021-test
new file mode 100644
index 0000000..bb76deb
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0021-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/qux; Path=/
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0022-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0022-expected
diff --git a/cookies/http-state/resources/test-files/attribute0022-test b/cookies/http-state/resources/test-files/attribute0022-test
new file mode 100644
index 0000000..ac79c0f
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0022-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/; Path=/qux
diff --git a/cookies/http-state/resources/test-files/attribute0023-expected b/cookies/http-state/resources/test-files/attribute0023-expected
new file mode 100644
index 0000000..b14d4f6
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0023-expected
@@ -0,0 +1 @@
+Cookie: foo=bar
diff --git a/cookies/http-state/resources/test-files/attribute0023-test b/cookies/http-state/resources/test-files/attribute0023-test
new file mode 100644
index 0000000..97f2ac3
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0023-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/qux; Path=/cookie-parser-result
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0024-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0024-expected
diff --git a/cookies/http-state/resources/test-files/attribute0024-test b/cookies/http-state/resources/test-files/attribute0024-test
new file mode 100644
index 0000000..cb041c5
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0024-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path=/cookie-parser-result; Path=/qux
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0025-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0025-expected
diff --git a/cookies/http-state/resources/test-files/attribute0025-test b/cookies/http-state/resources/test-files/attribute0025-test
new file mode 100644
index 0000000..c430943
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0025-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; qux; Secure
diff --git a/html/browsers/origin/.gitkeep b/cookies/http-state/resources/test-files/attribute0026-expected
similarity index 100%
copy from html/browsers/origin/.gitkeep
copy to cookies/http-state/resources/test-files/attribute0026-expected
diff --git a/cookies/http-state/resources/test-files/attribute0026-test b/cookies/http-state/resources/test-files/attribute0026-test
new file mode 100644
index 0000000..7f68322
--- /dev/null
+++ b/cookies/http-state/resources/test-files/attribute0026-test
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; qux="aaa;bbb"; Secure
diff --git a/css/.gitignore b/css/.gitignore
index 20072eb..3ed165f 100644
--- a/css/.gitignore
+++ b/css/.gitignore
@@ -2,6 +2,7 @@
 dist_last
 build-temp
 tools/cache
+tools/_virtualenv
 *.xcodeproj
 *.DS_Store
 *.pyc
diff --git a/css/CSS2/floats-clear/clear-on-parent-with-margins-no-clearance.html b/css/CSS2/floats-clear/clear-on-parent-with-margins-no-clearance.html
index 29ecc78..f3d3c18 100644
--- a/css/CSS2/floats-clear/clear-on-parent-with-margins-no-clearance.html
+++ b/css/CSS2/floats-clear/clear-on-parent-with-margins-no-clearance.html
@@ -4,7 +4,7 @@
 <link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#flow-control" title="9.5.2 Controlling flow next to floats: the 'clear' property">
 <link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
 <p>Test passes if there is a filled green square.</p>
-<div style="position:relative; z-index:-1; top:-50px; width:100px; background:green;">
+<div style="border-top:1px solid white; position:relative; z-index:-1; top:-51px; width:100px; background:green;">
   <div style="float:left; width:100px; height:50px; background:white;"></div>
   <div style="clear:left; margin-top:25px;">
     <div style="height:50px; margin-top:150px; background:white;"></div>
diff --git a/css/CSS2/floats-clear/floats-143-ref.xht b/css/CSS2/floats-clear/floats-143-ref.xht
index 49d1fe5..2fcdc13 100644
--- a/css/CSS2/floats-clear/floats-143-ref.xht
+++ b/css/CSS2/floats-clear/floats-143-ref.xht
@@ -9,6 +9,8 @@
   <link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/" />
 
   <style type="text/css"><![CDATA[
+  /* Disable kerning because kerning may differ for different node tree. */
+  html { font-kerning: none; font-feature-settings: "kern" off; }
   div
   {
   background-color: green;
diff --git a/css/CSS2/floats-clear/floats-143.xht b/css/CSS2/floats-clear/floats-143.xht
index 84acecc..6eb7e71 100644
--- a/css/CSS2/floats-clear/floats-143.xht
+++ b/css/CSS2/floats-clear/floats-143.xht
@@ -9,6 +9,8 @@
   <link rel="match" href="floats-143-ref.xht" />
 
   <style type="text/css">
+   /* Disable kerning because kerning may differ for different node tree. */
+   html { font-kerning: none; font-feature-settings: "kern" off; }
    .float { float: left; background: red; padding: 0; margin: 0; list-style: none; }
    .text { font: 900 2em/1 sans-serif; background: green; color: white; }
   </style>
diff --git a/css/CSS2/floats/new-fc-beside-adjoining-float-2.html b/css/CSS2/floats/new-fc-beside-adjoining-float-2.html
new file mode 100644
index 0000000..2d27a28
--- /dev/null
+++ b/css/CSS2/floats/new-fc-beside-adjoining-float-2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>A new formatting context that fits beside an adjoining float, and thus pulls down the float with its top margin</title>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#bfc-next-to-float" title="9.5 Floats">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#flow-control" title="9.5.2 Controlling flow next to floats: the 'clear' property">
+<meta name="assert" content="The float is adjoining with the box that establishes a new formatting context when it fits beside it, and will therefore be affected by its margin">
+<link rel="match" href="../../reference/ref-filled-green-200px-square.html">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="overflow:hidden; width:200px; height:200px; background:red;">
+  <div style="margin-top:190px;">
+    <div>
+      <div style="float:left; width:100px; height:200px; background:green;"></div>
+    </div>
+    <div style="margin-top:-190px; overflow:hidden; width:100px; height:200px; background:green;"></div>
+  </div>
+</div>
diff --git a/css/CSS2/floats/new-fc-beside-adjoining-float.html b/css/CSS2/floats/new-fc-beside-adjoining-float.html
new file mode 100644
index 0000000..91adbfc
--- /dev/null
+++ b/css/CSS2/floats/new-fc-beside-adjoining-float.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>A new formatting context that fits beside an adjoining float, and thus pulls down the float with its top margin</title>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#bfc-next-to-float" title="9.5 Floats">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#flow-control" title="9.5.2 Controlling flow next to floats: the 'clear' property">
+<meta name="assert" content="The float is adjoining with the box that establishes a new formatting context when it fits beside it, and will therefore be affected by its margin">
+<link rel="match" href="../../reference/ref-filled-green-200px-square.html">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="overflow:hidden; width:200px; background:green;">
+  <div style="width:300px; margin-top:50px; background:red;">
+    <div>
+      <div style="float:left; width:200px; height:10px; background:green;"></div>
+    </div>
+    <div style="margin-top:190px; overflow:hidden; width:100px; height:10px; background:red;"></div>
+  </div>
+</div>
diff --git a/css/CSS2/floats/new-fc-separates-from-float-2.html b/css/CSS2/floats/new-fc-separates-from-float-2.html
new file mode 100644
index 0000000..fd0deab
--- /dev/null
+++ b/css/CSS2/floats/new-fc-separates-from-float-2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>A new formatting context that doesn't fit beside a float make the float non-adjoining</title>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#bfc-next-to-float" title="9.5 Floats">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#flow-control" title="9.5.2 Controlling flow next to floats: the 'clear' property">
+<meta name="assert" content="Although the 'clear' property isn't specified in this test, a new formatting context that doesn't fit below a float that would otherwise be adjoining will need to separate its margin from the float, so that it doesn't affect the float. This is very similar to clearance.">
+<link rel="match" href="../../reference/ref-filled-green-200px-square.html">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="overflow:hidden; width:200px; height:200px; background:red;">
+  <div style="margin-top:-50px;">
+    <div>
+      <div style="float:left; width:200px; height:150px; background:green;"></div>
+    </div>
+    <div style="margin-top:12345px; overflow:hidden; width:200px; height:100px; background:green;"></div>
+  </div>
+</div>
diff --git a/css/CSS2/floats/new-fc-separates-from-float.html b/css/CSS2/floats/new-fc-separates-from-float.html
new file mode 100644
index 0000000..89ee751
--- /dev/null
+++ b/css/CSS2/floats/new-fc-separates-from-float.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>A new formatting context that doesn't fit beside a float make the float non-adjoining</title>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#bfc-next-to-float" title="9.5 Floats">
+<link rel="help" href="https://www.w3.org/TR/CSS22/visuren.html#flow-control" title="9.5.2 Controlling flow next to floats: the 'clear' property">
+<meta name="assert" content="Although the 'clear' property isn't specified in this test, a new formatting context that doesn't fit below a float that would otherwise be adjoining will need to separate its margin from the float, so that it doesn't affect the float. This is very similar to clearance.">
+<link rel="match" href="../../reference/ref-filled-green-200px-square.html">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="overflow:hidden; width:200px; background:red;">
+  <div>
+    <div>
+      <div style="float:left; width:200px; height:200px; background:green;"></div>
+    </div>
+    <div style="margin-top:200px; overflow:hidden; width:200px; height:1px; background:white;"></div>
+  </div>
+</div>
diff --git a/css/CSS2/zorder/z-index-020-ref2.xht b/css/CSS2/zorder/z-index-020-ref2.xht
new file mode 100644
index 0000000..936116c
--- /dev/null
+++ b/css/CSS2/zorder/z-index-020-ref2.xht
@@ -0,0 +1,102 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <title>CSS Reference: z-index (option B)</title>
+    <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact"/>
+    <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
+    <meta name="flags" content="" />
+<style type="text/css">
+.container {
+  z-index:0;
+  position: relative;
+  height: 200px;
+  width: 200px;
+  font-size: 0;
+  line-height: 0;
+  background: silver;
+  border: solid white;
+}
+.container div {
+  height: 80px;
+  width: 80px;
+  padding: 10px;
+}
+
+.control .outline {
+  border: solid fuchsia 5px;
+  width: 110px;
+  height: 85px;
+  padding: 0;
+}
+
+.outline.c1 {
+  margin: 30px 5px 100px;
+}
+.outline.c2 {
+  padding: 0;
+  margin: -20px -45px;
+}
+.outline.c2 > div {
+  margin: -20px 5px 0;
+}
+
+.control div {
+  margin-left: -35px;
+  margin-top: -35px;
+}
+.control > div {
+  margin-left: auto;
+  margin-top: 100px;
+}
+</style>
+</head>
+<body>
+
+<div class="control container">
+  <div style="background: navy">
+    <div style="background: blue">
+       <div style="background: aqua">
+         <div class="outline c2">
+           <div style="background: lime">
+             <div style="background: teal">
+             </div>
+           </div>
+         </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<p>The pattern above must match one of the two patterns below.</p>
+
+<div class="control container">
+  <div style="background: navy">
+    <div style="background: blue">
+       <div style="background: aqua">
+         <div style="background: lime">
+           <div style="background: teal">
+             <div class="outline c1">
+             </div>
+           </div>
+         </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="control container">
+  <div style="background: navy">
+    <div style="background: blue">
+       <div style="background: aqua">
+         <div class="outline c2">
+           <div style="background: lime">
+             <div style="background: teal">
+             </div>
+           </div>
+         </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+</body></html>
diff --git a/css/CSS2/zorder/z-index-020.xht b/css/CSS2/zorder/z-index-020.xht
index c50cc59..d882d85 100644
--- a/css/CSS2/zorder/z-index-020.xht
+++ b/css/CSS2/zorder/z-index-020.xht
@@ -7,6 +7,7 @@
     <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" />
     <link rel="help" href="http://www.w3.org/TR/CSS21/visuren.html#z-index" />
     <link rel="match" href="z-index-020-ref.xht"/>
+    <link rel="match" href="z-index-020-ref2.xht"/>
     <meta name="flags" content="" />
 <style type="text/css">
 .container {
diff --git a/css/css-align/content-distribution/place-content-shorthand-001.html b/css/css-align/content-distribution/place-content-shorthand-001.html
index 153f6dd..1506333 100644
--- a/css/css-align/content-distribution/place-content-shorthand-001.html
+++ b/css/css-align/content-distribution/place-content-shorthand-001.html
@@ -8,7 +8,7 @@
 <script src="/css/css-align/resources/alignment-parsing-utils.js"></script>
 <div id="log"></div>
 <script>
-    let classes = Object.assign({"Normal":"normal"}, contentPositionClasses, distributionClasses);
+    let classes = Object.assign({"Normal":"normal"}, contentPositionClasses, distributionClasses, overflowClasses);
     for (var key in classes) {
         let value = classes[key];
         test(function() {
diff --git a/css/css-align/content-distribution/place-content-shorthand-002.html b/css/css-align/content-distribution/place-content-shorthand-002.html
index e8a7b33..589feff 100644
--- a/css/css-align/content-distribution/place-content-shorthand-002.html
+++ b/css/css-align/content-distribution/place-content-shorthand-002.html
@@ -8,10 +8,12 @@
 <script src="/css/css-align/resources/alignment-parsing-utils.js"></script>
 <div id="log"></div>
 <script>
-    let classes = Object.assign({"Normal":"normal"}, contentPositionClasses, distributionClasses, baselineClasses);
+    let classes = Object.assign({"Normal":"normal"}, contentPositionClasses, distributionClasses, baselineClasses,
+                                overflowClasses);
     for (var key1 in classes) {
         let alignValue = classes[key1];
-        let classes2 = Object.assign({"Normal":"normal", "Left":"left", "Right":"right"}, contentPositionClasses, distributionClasses);
+        let classes2 = Object.assign({"Normal":"normal", "Left":"left", "Right":"right"}, contentPositionClasses,
+                                     distributionClasses);
         for (var key2 in classes2) {
             let justifyValue = classes2[key2];
             test(function() {
diff --git a/css/css-align/default-alignment/parse-justify-items-001.html b/css/css-align/default-alignment/parse-justify-items-001.html
index aa4b147..b2edd2a 100644
--- a/css/css-align/default-alignment/parse-justify-items-001.html
+++ b/css/css-align/default-alignment/parse-justify-items-001.html
@@ -15,7 +15,7 @@
 <div id="log"></div>
 <script>
     let classes = Object.assign({"Normal":"normal", "Stretch":"stretch", "Left":"left", "Right":"right"},
-                                selfPositionClasses, baselineClasses, overflowClasses);
+                                selfPositionClasses, baselineClasses, overflowClasses, legacyClasses);
 
     for (var key in classes) {
         let specifiedValue = classes[key];
diff --git a/css/css-align/default-alignment/parse-justify-items-003.html b/css/css-align/default-alignment/parse-justify-items-003.html
index 9ab5ee3..6da7d76 100644
--- a/css/css-align/default-alignment/parse-justify-items-003.html
+++ b/css/css-align/default-alignment/parse-justify-items-003.html
@@ -17,7 +17,7 @@
     document.body.appendChild(element);
 
     let classes = Object.assign({"Normal":"normal", "Stretch":"stretch", "Left":"left", "Right":"right"},
-                                selfPositionClasses, baselineClasses, overflowClasses);
+                                selfPositionClasses, baselineClasses, overflowClasses, legacyClasses);
 
     for (var key in classes) {
         let specifiedValue = classes[key];
diff --git a/css/css-align/default-alignment/place-items-shorthand-001.html b/css/css-align/default-alignment/place-items-shorthand-001.html
index c67ca9c..5b63c87 100644
--- a/css/css-align/default-alignment/place-items-shorthand-001.html
+++ b/css/css-align/default-alignment/place-items-shorthand-001.html
@@ -8,7 +8,8 @@
 <script src="/css/css-align/resources/alignment-parsing-utils.js"></script>
 <div id="log"></div>
 <script>
-    let classes = Object.assign({"Normal":"normal", "Stretch":"stretch"}, selfPositionClasses, baselineClasses);
+    let classes = Object.assign({"Normal":"normal", "Stretch":"stretch"}, selfPositionClasses, baselineClasses,
+                                overflowClasses);
     for (var key in classes) {
         let value = classes[key];
         test(function() {
diff --git a/css/css-align/default-alignment/place-items-shorthand-002.html b/css/css-align/default-alignment/place-items-shorthand-002.html
index 61dab31..3a075b2 100644
--- a/css/css-align/default-alignment/place-items-shorthand-002.html
+++ b/css/css-align/default-alignment/place-items-shorthand-002.html
@@ -8,10 +8,11 @@
 <script src="/css/css-align/resources/alignment-parsing-utils.js"></script>
 <div id="log"></div>
 <script>
-    let classes = Object.assign({"Normal":"normal", "Stretch":"stretch"}, selfPositionClasses, baselineClasses);
+    let classes = Object.assign({"Normal":"normal", "Stretch":"stretch"}, selfPositionClasses, baselineClasses,
+                                overflowClasses);
     for (var key1 in classes) {
         let alignValue = classes[key1];
-        let classes2 = Object.assign({"Left":"left", "Right":"right"}, classes);
+        let classes2 = Object.assign({"Left":"left", "Right":"right"}, legacyClasses, classes);
         for (var key2 in classes2) {
            let justifyValue = classes2[key2];
            test(function() {
diff --git a/css/css-align/default-alignment/place-items-shorthand-004.html b/css/css-align/default-alignment/place-items-shorthand-004.html
index b70ddbe..f0d245c 100644
--- a/css/css-align/default-alignment/place-items-shorthand-004.html
+++ b/css/css-align/default-alignment/place-items-shorthand-004.html
@@ -14,7 +14,7 @@
     }
 
     test(function() {
-        checkInvalidValues("center space-between start")
+        checkInvalidValues("center end start")
     }, "Verify fallback values are invalid");
 
     test(function() {
diff --git a/css/css-align/default-alignment/shorthand-serialization-001.html b/css/css-align/default-alignment/shorthand-serialization-001.html
index 0145ec1..730b2ad 100644
--- a/css/css-align/default-alignment/shorthand-serialization-001.html
+++ b/css/css-align/default-alignment/shorthand-serialization-001.html
@@ -17,7 +17,7 @@
     alignItems: "normal",
     alignSelf: "auto",
     justifyContent: "normal",
-    justifyItems: "auto",
+    justifyItems: "legacy",
     justifySelf: "auto",
 };
 
@@ -52,11 +52,11 @@
 var place_items_test_cases = [
     {
         alignItems: "center",
-        shorthand: "center auto",
+        shorthand: "center legacy",
     },
     {
         alignItems: "baseline",
-        shorthand: "baseline auto",
+        shorthand: "baseline legacy",
     },
     {
         justifyItems: "safe start",
diff --git a/css/css-align/resources/alignment-parsing-utils.js b/css/css-align/resources/alignment-parsing-utils.js
index 0cbb820..f549bed 100644
--- a/css/css-align/resources/alignment-parsing-utils.js
+++ b/css/css-align/resources/alignment-parsing-utils.js
@@ -3,7 +3,7 @@
 var distributionClasses = {"Stretch":"stretch", "SpaceAround":"space-around", "SpaceBetween":"space-between", "SpaceEvenly":"space-evenly"};
 var baselineClasses = {"Baseline":"baseline", "FirstBaseline":"first baseline", "LastBaseline":"last baseline"};
 var overflowClasses = {"SafeFlexEnd":"safe flex-end", "UnsafeEnd":"unsafe end", "SafeEnd":"safe end", "UnsafeFlexStart":"unsafe flex-start", "SafeCenter":"safe center"};
-var legacyClasses = {"LegacyLeft":"legacy left", "LegacyCenter":"legacy center", "LegacyRight":"legacy right", "LeftLegacy":"left legacy", "CenterLegacy":"center legacy", "RightLegacy":"right legacy", "Legacy":"legacy"};
+var legacyClasses = {"LegacyLeft":"legacy left", "LegacyCenter":"legacy center", "LegacyRight":"legacy right"};
 
 var invalidPositionValues = ["auto safe", "auto left", "normal unsafe", "normal stretch", "baseline normal",
                              "baseline center", "first baseline center", "last baseline center", "baseline last",
diff --git a/css/css-align/self-alignment/place-self-shorthand-001.html b/css/css-align/self-alignment/place-self-shorthand-001.html
index d26df05..bd95302 100644
--- a/css/css-align/self-alignment/place-self-shorthand-001.html
+++ b/css/css-align/self-alignment/place-self-shorthand-001.html
@@ -9,7 +9,7 @@
 <div id="log"></div>
 <script>
     let classes = Object.assign({"Auto":"auto", "Normal":"normal", "Stretch":"stretch"}, selfPositionClasses,
-                                baselineClasses);
+                                baselineClasses, overflowClasses);
     for (var key in classes) {
         let value = classes[key];
         test(function() {
diff --git a/css/css-align/self-alignment/place-self-shorthand-002.html b/css/css-align/self-alignment/place-self-shorthand-002.html
index 3ba6c55..47bb78e 100644
--- a/css/css-align/self-alignment/place-self-shorthand-002.html
+++ b/css/css-align/self-alignment/place-self-shorthand-002.html
@@ -9,7 +9,7 @@
 <div id="log"></div>
 <script>
     let classes = Object.assign({"Auto":"auto", "Normal":"normal", "Stretch":"stretch"}, selfPositionClasses,
-                                baselineClasses);
+                                baselineClasses, overflowClasses);
     for (var key1 in classes) {
         let alignValue = classes[key1];
         let classes2 = Object.assign({"Left":"left", "Right":"right"}, classes);
diff --git a/css/css-align/self-alignment/place-self-shorthand-004.html b/css/css-align/self-alignment/place-self-shorthand-004.html
index 2e87b06..b7c9d80 100644
--- a/css/css-align/self-alignment/place-self-shorthand-004.html
+++ b/css/css-align/self-alignment/place-self-shorthand-004.html
@@ -14,7 +14,7 @@
     }
 
     test(function() {
-        checkInvalidValues("center space-between start")
+        checkInvalidValues("center end start")
     }, "Verify fallback values are invalid");
 
     test(function() {
diff --git a/css/css-backgrounds/background-rounded-image-clip.html b/css/css-backgrounds/background-rounded-image-clip.html
new file mode 100644
index 0000000..1179ba5
--- /dev/null
+++ b/css/css-backgrounds/background-rounded-image-clip.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Background Clip Follows Rounded Corner</title>
+<link rel="match" href="reference/background-rounded-image-clip.html">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds-3/#corner-clipping">
+<style>
+    html {
+        background-color: green;
+    }
+    #a {
+        top: 20px;
+        left: 20px;
+        position: absolute;
+        width: 20px;
+        height: 20px;
+        background-color: black;
+    }
+
+    #b {
+        position: absolute;
+        width: 300px;
+        height: 200px;
+        background-image: linear-gradient(green, green);
+        background-clip: content-box;
+        border-top-left-radius: 90px;
+        border-width: 10px;
+        border-style: solid;
+        border-color: transparent;
+    }
+</style>
+<div id="a"></div>
+<div id="b"></div>
diff --git a/css/css-backgrounds/parsing/background-attachment-invalid.html b/css/css-backgrounds/parsing/background-attachment-invalid.html
new file mode 100644
index 0000000..e4924a2
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-attachment-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-attachment with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-attachment">
+<meta name="assert" content="background-attachment supports only the grammar '<attachment>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-attachment", "auto");
+test_invalid_value("background-attachment", "local, none");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-attachment-valid.html b/css/css-backgrounds/parsing/background-attachment-valid.html
new file mode 100644
index 0000000..4c7243f
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-attachment-valid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-attachment with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-attachment">
+<meta name="assert" content="background-attachment supports the full grammar '<attachment>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-attachment", "fixed");
+test_valid_value("background-attachment", "scroll, fixed, local, fixed, scroll");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-clip-invalid.html b/css/css-backgrounds/parsing/background-clip-invalid.html
new file mode 100644
index 0000000..50647e8
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-clip-invalid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-clip with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-clip">
+<meta name="assert" content="background-clip supports only the grammar '<box>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-clip", "fill-box");
+test_invalid_value("background-clip", "margin-box");
+test_invalid_value("background-clip", "stroke-box");
+test_invalid_value("background-clip", "view-box");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-clip-valid.html b/css/css-backgrounds/parsing/background-clip-valid.html
new file mode 100644
index 0000000..2b6bd49
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-clip-valid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-clip with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-clip">
+<meta name="assert" content="background-clip supports the full grammar '<box>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-clip", "border-box");
+test_valid_value("background-clip", "padding-box");
+test_valid_value("background-clip", "content-box");
+
+test_valid_value("background-clip", "border-box, padding-box, content-box");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-color-invalid.html b/css/css-backgrounds/parsing/background-color-invalid.html
new file mode 100644
index 0000000..cc31e26
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-color-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-color with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-color">
+<meta name="assert" content="background-color supports only the grammar '<color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-color", "none");
+test_invalid_value("background-color", "black white");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-color-valid.html b/css/css-backgrounds/parsing/background-color-valid.html
new file mode 100644
index 0000000..e5f582f
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-color-valid.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-color with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-color">
+<meta name="assert" content="background-color supports the full grammar '<color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Edge serializes as "currentColor".
+test_valid_value("background-color", "currentcolor", ["currentcolor", "currentColor"]);
+test_valid_value("background-color", "currentColor", ["currentcolor", "currentColor"]);
+
+test_valid_value("background-color", "red");
+test_valid_value("background-color", "#00FF00", "rgb(0, 255, 0)");
+test_valid_value("background-color", "rgb(0, 0, 255)");
+test_valid_value("background-color", "rgb(100%, 100%, 0%)", "rgb(255, 255, 0)");
+test_valid_value("background-color", "hsl(120, 100%, 50%)", ["rgb(0, 255, 0)", "hsl(120, 100%, 50%)"]); // Edge serializes as hsl
+test_valid_value("background-color", "teal");
+
+test_valid_value("background-color", "transparent");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-image-invalid.html b/css/css-backgrounds/parsing/background-image-invalid.html
new file mode 100644
index 0000000..a5f2a90
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-image-invalid.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-image with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-image">
+<meta name="assert" content="background-image supports only the grammar '<bg-image>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-image", "none, auto");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-image-valid.html b/css/css-backgrounds/parsing/background-image-valid.html
new file mode 100644
index 0000000..873800f
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-image-valid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-image with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-image">
+<meta name="assert" content="background-image supports the full grammar '<bg-image>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-image", "none");
+
+// Safari removes quotes.
+test_valid_value("background-image", 'url("http://www.example.com/")', ['url("http://www.example.com/")', 'url(http://www.example.com/)']);
+test_valid_value("background-image", 'none, url("http://www.example.com/")', ['none, url("http://www.example.com/")', 'none, url(http://www.example.com/)']);
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-invalid.html b/css/css-backgrounds/parsing/background-invalid.html
new file mode 100644
index 0000000..adc178b
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-invalid.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background">
+<meta name="assert" content="background supports only the grammar '<bg-layer># , <final-bg-layer>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background", "red, green");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-origin-invalid.html b/css/css-backgrounds/parsing/background-origin-invalid.html
new file mode 100644
index 0000000..5458925
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-origin-invalid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-origin with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-origin">
+<meta name="assert" content="background-origin supports only the grammar '<box>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-origin", "fill-box");
+test_invalid_value("background-origin", "margin-box");
+test_invalid_value("background-origin", "stroke-box");
+test_invalid_value("background-origin", "view-box");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-origin-valid.html b/css/css-backgrounds/parsing/background-origin-valid.html
new file mode 100644
index 0000000..ff52126
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-origin-valid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-origin with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-origin">
+<meta name="assert" content="background-origin supports the full grammar '<box>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-origin", "border-box");
+test_valid_value("background-origin", "padding-box");
+test_valid_value("background-origin", "content-box");
+
+test_valid_value("background-origin", "border-box, padding-box, content-box");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-position-invalid.html b/css/css-backgrounds/parsing/background-position-invalid.html
new file mode 100644
index 0000000..5ddf5e0
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-position-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-position with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-position">
+<meta name="assert" content="background-position supports only the grammar '<bg-position>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-position", "left right");
+test_invalid_value("background-position", "top bottom");
+test_invalid_value("background-position", "1% center 2px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-position-valid.html b/css/css-backgrounds/parsing/background-position-valid.html
new file mode 100644
index 0000000..836f348
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-position-valid.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-position with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-position">
+<meta name="assert" content="background-position supports the full grammar '<bg-position>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-position", "1px", ["1px", "1px center"]);
+test_valid_value("background-position", "1px center", ["1px", "1px center"]);
+test_valid_value("background-position", "-2% -3%");
+test_valid_value("background-position", "5% top");
+test_valid_value("background-position", "center", ["center", "center center"]);
+test_valid_value("background-position", "center center", ["center", "center center"]);
+test_valid_value("background-position", "center 6px");
+test_valid_value("background-position", "center left", ["left center", "left"]);
+test_valid_value("background-position", "center right 7%", "right 7% center");
+test_valid_value("background-position", "center bottom", ["center bottom", "bottom"]);
+test_valid_value("background-position", "center top 8px", ["center top 8px", "center 8px"]);
+test_valid_value("background-position", "left", ["left center", "left"]);
+test_valid_value("background-position", "right 9%");
+test_valid_value("background-position", "left 10px center", ["left 10px center", "10px"]);
+test_valid_value("background-position", "right 11% bottom", ["right 11% bottom", "right 11% bottom 0%"]); // "right 11% bottom 0%" in Edge
+test_valid_value("background-position", "left 12px top 13px", ["left 12px top 13px", "12px 13px"]);
+test_valid_value("background-position", "right center", ["right center", "right"]);
+test_valid_value("background-position", "left bottom");
+test_valid_value("background-position", "right top 14%", ["right top 14%", "right 14%"]);
+test_valid_value("background-position", "bottom", ["center bottom", "bottom"]);
+test_valid_value("background-position", "top 15px center", ["center top 15px", "center 15px"]);
+test_valid_value("background-position", "bottom 16% left", ["left bottom 16%", "left 0% bottom 16%"]); // "left 0% bottom 16%" in Edge
+test_valid_value("background-position", "top 17px right 18px", "right 18px top 17px");
+test_valid_value("background-position", "bottom center", ["center bottom", "bottom"]);
+test_valid_value("background-position", "top left", "left top");
+test_valid_value("background-position", "bottom right 19%", ["right 19% bottom", "right 19% bottom 0%"]); // "right 19% bottom 0%" in Edge
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-repeat-invalid.html b/css/css-backgrounds/parsing/background-repeat-invalid.html
new file mode 100644
index 0000000..a32f508
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-repeat-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-repeat with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-repeat">
+<meta name="assert" content="background-repeat supports only the grammar '<repeat-style>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("background-repeat", "repeat repeat-x");
+test_invalid_value("background-repeat", "repeat-y round");
+test_invalid_value("background-repeat", "repeat space round");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-repeat-valid.html b/css/css-backgrounds/parsing/background-repeat-valid.html
new file mode 100644
index 0000000..7554909
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-repeat-valid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-repeat with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-repeat">
+<meta name="assert" content="background-repeat supports the full grammar '<repeat-style>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-repeat", "repeat");
+test_valid_value("background-repeat", "repeat-x, repeat-y, repeat", "repeat-x, repeat-y, repeat");
+test_valid_value("background-repeat", "repeat space, round no-repeat, repeat-x");
+test_valid_value("background-repeat", "repeat repeat", ["repeat", "repeat repeat"]);
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-size-invalid.html b/css/css-backgrounds/parsing/background-size-invalid.html
new file mode 100644
index 0000000..7259cbd
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-size-invalid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-size with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-size">
+<meta name="assert" content="background-size supports only the grammar '<bg-size>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Blink and WebKit fail these by accepting negative values.
+test_invalid_value("background-size", "-1px");
+test_invalid_value("background-size", "2% -3%");
+
+test_invalid_value("background-size", "1px 2px 3px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-size-valid.html b/css/css-backgrounds/parsing/background-size-valid.html
new file mode 100644
index 0000000..6703164
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-size-valid.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background-size with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-size">
+<meta name="assert" content="background-size supports the full grammar '<bg-size>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("background-size", "1px", ["1px", "1px auto"]);
+test_valid_value("background-size", "1px auto", ["1px", "1px auto"]);
+test_valid_value("background-size", "2% 3%");
+test_valid_value("background-size", "auto", ["auto", "auto auto"]);
+test_valid_value("background-size", "auto auto", ["auto", "auto auto"]);
+test_valid_value("background-size", "auto 4%");
+test_valid_value("background-size", "contain");
+test_valid_value("background-size", "cover");
+
+test_valid_value("background-size", "auto 1px, 2% 3%, contain");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/background-valid.html b/css/css-backgrounds/parsing/background-valid.html
new file mode 100644
index 0000000..29737e2
--- /dev/null
+++ b/css/css-backgrounds/parsing/background-valid.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing background with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background">
+<meta name="assert" content="background supports the full grammar '<bg-layer># , <final-bg-layer>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Background serialization varies across browsers. https://github.com/w3c/csswg-drafts/issues/418
+
+test_valid_value("background",
+  'url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box, rgb(5, 6, 7) url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box', [
+  'url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box, rgb(5, 6, 7) url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box', // spec
+  'url("https://example.com/") local space round 1px 2px / 3px 4px padding-box content-box, url("https://example.com/") local space round 1px 2px / 3px 4px padding-box content-box rgb(5, 6, 7)', // Edge
+  'url("https://example.com/") space round local 1px 2px / 3px 4px padding-box content-box, rgb(5, 6, 7) url("https://example.com/") space round local 1px 2px / 3px 4px padding-box content-box', // Firefox
+  'url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box, url("https://example.com/") 1px 2px / 3px 4px space round local padding-box content-box rgb(5, 6, 7)', // Blink
+  'url(https://example.com/) 1px 2px / 3px 4px space round local padding-box content-box, url(https://example.com/) 1px 2px / 3px 4px space round local padding-box content-box rgb(5, 6, 7)' // WebKit omits quotes - https://bugs.webkit.org/show_bug.cgi?id=28869
+]);
+
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-color-invalid.html b/css/css-backgrounds/parsing/border-color-invalid.html
new file mode 100644
index 0000000..5c63d8b
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-color-invalid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-color with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-color">
+<meta name="assert" content="border-color supports only the grammar '<color>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-color", "auto");
+
+test_invalid_value("border-color", "black, white");
+
+test_invalid_value("border-color", "black white red green blue");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-color-valid.html b/css/css-backgrounds/parsing/border-color-valid.html
new file mode 100644
index 0000000..be78a06
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-color-valid.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-color with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-color">
+<meta name="assert" content="border-color supports the full grammar '<color>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Edge serializes as "currentColor".
+test_valid_value("border-color", "currentcolor", ["currentcolor", "currentColor"]);
+test_valid_value("border-color", "currentColor", ["currentcolor", "currentColor"]);
+
+test_valid_value("border-color", "red yellow green blue");
+
+test_valid_value("border-top-color", "red");
+test_valid_value("border-right-color", "yellow");
+test_valid_value("border-bottom-color", "green");
+test_valid_value("border-left-color", "blue");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-invalid.html b/css/css-backgrounds/parsing/border-image-invalid.html
new file mode 100644
index 0000000..762be9a
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-invalid.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image">
+<meta name="assert" content="border-image supports only the grammar ' <‘border-image-source’> || <‘border-image-slice’> [ / <‘border-image-width’> | / <‘border-image-width’>? / <‘border-image-outset’> ]? || <‘border-image-repeat’>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image", "auto");
+test_invalid_value("border-image", 'none, url("http://www.example.com/")');
+
+test_invalid_value("border-image", "stretch repeat round");
+
+test_invalid_value("border-image", "fill");
+test_invalid_value("border-image", "1 2 3 4 5");
+test_invalid_value("border-image", "1% fill 2%");
+
+test_invalid_value("border-image", "1 / -2px");
+test_invalid_value("border-image", "-1 / 2px");
+
+test_invalid_value("border-image", "1 / 1 2 3 4 5");
+
+test_invalid_value("border-image", "1 2 3 4 5 / / 1px");
+test_invalid_value("border-image", "1 / / auto");
+test_invalid_value("border-image", "1 2% 3 4% / / 1%");
+test_invalid_value("border-image", "1 2% 3 4% fill / / 1 2 3 4 5");
+
+test_invalid_value("border-image", "1 / none / 1px");
+test_invalid_value("border-image", "1 2% 3 4% / 1 2 3 4 5 / 2");
+test_invalid_value("border-image", "1 2 3 4 5 / 1px / 1px");
+test_invalid_value("border-image", "1 / 1px / auto");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-outset-invalid.html b/css/css-backgrounds/parsing/border-image-outset-invalid.html
new file mode 100644
index 0000000..ffe0959
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-outset-invalid.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-outset with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-outset">
+<meta name="assert" content="border-image-outset supports only the grammar '[ <length> | <number> ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image-outset", "auto");
+
+test_invalid_value("border-image-outset", "-1");
+test_invalid_value("border-image-outset", "-2px");
+
+test_invalid_value("border-image-outset", "1%");
+
+test_invalid_value("border-image-outset", "1 2 3 4 5");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-outset-valid.html b/css/css-backgrounds/parsing/border-image-outset-valid.html
new file mode 100644
index 0000000..dc18937
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-outset-valid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-outset with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-outset">
+<meta name="assert" content="border-image-outset supports the full grammar '[ <length> | <number> ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-image-outset", "1px");
+test_valid_value("border-image-outset", "2");
+test_valid_value("border-image-outset", "1px 2 3px 4");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-repeat-invalid.html b/css/css-backgrounds/parsing/border-image-repeat-invalid.html
new file mode 100644
index 0000000..b17b531
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-repeat-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-repeat with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-repeat">
+<meta name="assert" content="border-image-repeat supports only the grammar '[ stretch | repeat | round | space ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image-repeat", "auto");
+
+test_invalid_value("border-image-repeat", "stretch repeat round");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-repeat-valid.html b/css/css-backgrounds/parsing/border-image-repeat-valid.html
new file mode 100644
index 0000000..6e13dc8
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-repeat-valid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-repeat with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-repeat">
+<meta name="assert" content="border-image-repeat supports the full grammar '[ stretch | repeat | round | space ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-image-repeat", "stretch");
+test_valid_value("border-image-repeat", "space space", "space");
+test_valid_value("border-image-repeat", "repeat round");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-slice-invalid.html b/css/css-backgrounds/parsing/border-image-slice-invalid.html
new file mode 100644
index 0000000..7bdb67f
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-slice-invalid.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-slice with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-slice">
+<meta name="assert" content="border-image-slice supports only the grammar '[<number> | <percentage>]{1,4} && fill?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image-slice", "fill");
+
+test_invalid_value("border-image-slice", "1 -2% fill");
+test_invalid_value("border-image-slice", "-1 2% fill");
+
+test_invalid_value("border-image-slice", "1 2 3 4 5");
+
+test_invalid_value("border-image-slice", "1% fill 2%");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-slice-valid.html b/css/css-backgrounds/parsing/border-image-slice-valid.html
new file mode 100644
index 0000000..257f276
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-slice-valid.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-slice with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-slice">
+<meta name="assert" content="border-image-slice supports the full grammar '[<number> | <percentage>]{1,4} && fill?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-image-slice", "1");
+test_valid_value("border-image-slice", "1 2% 3 4%");
+
+test_valid_value("border-image-slice", "1 2% 3 4% fill");
+test_valid_value("border-image-slice", "fill 1 2% 3 4%", "1 2% 3 4% fill");
+
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-source-invalid.html b/css/css-backgrounds/parsing/border-image-source-invalid.html
new file mode 100644
index 0000000..99a06ce
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-source-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-source with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-source">
+<meta name="assert" content="border-image-source supports only the grammar 'none | <image>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image-source", "auto");
+
+test_invalid_value("border-image-source", 'none, url("http://www.example.com/")');
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-source-valid.html b/css/css-backgrounds/parsing/border-image-source-valid.html
new file mode 100644
index 0000000..e12cb92
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-source-valid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-source with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-source">
+<meta name="assert" content="border-image-source supports the full grammar 'none | <image>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-image-source", "none");
+
+// Safari removes quotes.
+test_valid_value("border-image-source", 'url("http://www.example.com/")', ['url("http://www.example.com/")', 'url(http://www.example.com/)']);
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-valid.html b/css/css-backgrounds/parsing/border-image-valid.html
new file mode 100644
index 0000000..79bff45
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-valid.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image">
+<meta name="assert" content="border-image supports the full grammar ' <‘border-image-source’> || <‘border-image-slice’> [ / <‘border-image-width’> | / <‘border-image-width’>? / <‘border-image-outset’> ]? || <‘border-image-repeat’>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// WebKit fails all these tests by returning an empty string as the value of border-image.
+
+// "none" in Edge, "none 100% / 1 / 0 stretch" in Firefox and Blink.
+test_valid_value("border-image", "none", ["none", "none 100% / 1 / 0 stretch"]);
+test_valid_value("border-image", "stretch", ["none", "none 100% / 1 / 0 stretch"]);
+test_valid_value("border-image", "none 100% / 1 / 0 stretch", ["none", "none 100% / 1 / 0 stretch"]);
+
+test_valid_value("border-image", 'url("http://www.example.com/") 1 2 3 4 fill', ['url("http://www.example.com/") 1 2 3 4 fill', 'url("http://www.example.com/") 1 2 3 4 fill / 1 / 0 stretch']);
+test_valid_value("border-image", 'url("http://www.example.com/") 1 2 3 4 fill / 1 / 0 stretch', ['url("http://www.example.com/") 1 2 3 4 fill', 'url("http://www.example.com/") 1 2 3 4 fill / 1 / 0 stretch']);
+
+test_valid_value("border-image", 'url("http://www.example.com/")', ['url("http://www.example.com/")', 'url("http://www.example.com/") 100% / 1 / 0 stretch']);
+
+test_valid_value("border-image", "repeat round", ["repeat round", "none repeat round", "none 100% / 1 / 0 repeat round"]);
+test_valid_value("border-image", "none repeat round", ["repeat round", "none repeat round", "none 100% / 1 / 0 repeat round"]);
+
+test_valid_value("border-image", "space", ["space", "none space space", "none 100% / 1 / 0 space"]);
+test_valid_value("border-image", "none space space", ["space", "none space space", "none 100% / 1 / 0 space"]);
+test_valid_value("border-image", "none 100% / 1 / 0 space", ["space", "none space space", "none 100% / 1 / 0 space"]);
+
+test_valid_value("border-image", "1", ["1", "none 1 1 1 1", "none 1 / 1 / 0 stretch"]);
+test_valid_value("border-image", "none 1 1 1 1", ["1", "none 1 1 1 1", "none 1 / 1 / 0 stretch"]);
+test_valid_value("border-image", "none 1 / 1 / 0 stretch", ["1", "none 1 1 1 1", "none 1 / 1 / 0 stretch"]);
+
+test_valid_value("border-image", 'url("http://www.example.com/") 1 2% 3 4%', ['url("http://www.example.com/") 1 2% 3 4%', 'url("http://www.example.com/") 1 2% 3 4% / 1 / 0 stretch']);
+test_valid_value("border-image", 'url("http://www.example.com/") 1 2% 3 4% fill', ['url("http://www.example.com/") 1 2% 3 4% fill', 'url("http://www.example.com/") 1 2% 3 4% fill / 1 / 0 stretch']);
+test_valid_value("border-image", 'url("http://www.example.com/") fill 1 2% 3 4%', ['url("http://www.example.com/") 1 2% 3 4% fill', 'url("http://www.example.com/") 1 2% 3 4% fill / 1 / 0 stretch']);
+
+test_valid_value("border-image", "1 / 1px", ["1 / 1px", "none 1 / 1px / 0 stretch"]);
+test_valid_value("border-image", "1 2% 3 4% / 2%", ["1 2% 3 4% / 2%", "none 1 2% 3 4% / 2% 2% 2% 2%", "none 1 2% 3 4% / 2% / 0 stretch"]);
+test_valid_value("border-image", "1 2% 3 4% fill / 3", ["1 2% 3 4% fill / 3", "none 1 2% 3 4% fill / 3 3 3 3", "none 1 2% 3 4% fill / 3 / 0 stretch"]);
+test_valid_value("border-image", "fill 1 2% 3 4% / auto", ["1 2% 3 4% fill / auto", "none 1 2% 3 4% fill / auto / 0 stretch"]);
+test_valid_value("border-image", "1 / 1px 2% 3 auto", ["1 / 1px 2% 3 auto", "none 1 / 1px 2% 3 auto / 0 stretch"]);
+
+test_valid_value("border-image", "1 / / 1px", ["1 / / 1px", "none 1 / 1 / 1px stretch"]);
+test_valid_value("border-image", "1 2% 3 4% / / 2", ["1 2% 3 4% / / 2", "none 1 2% 3 4% / 1 / 2 stretch"]);
+test_valid_value("border-image", 'url("http://www.example.com/") 1 2% 3 4% fill / / 1px 2 3px 4', ['url("http://www.example.com/") 1 2% 3 4% fill / / 1px 2 3px 4', 'url("http://www.example.com/") 1 2% 3 4% fill / 1 / 1px 2 3px 4 stretch']);
+
+test_valid_value("border-image", "1 / 1px / 1px", ["1 / 1px / 1px", "none 1 / 1px / 1px stretch"]);
+test_valid_value("border-image", "1 2% 3 4% / 2% / 2", ["1 2% 3 4% / 2% / 2", "none 1 2% 3 4% / 2% / 2 stretch"]);
+test_valid_value("border-image", "1 2% 3 4% fill / 3 / 1px 2 3px 4", ["1 2% 3 4% fill / 3 / 1px 2 3px 4", "none 1 2% 3 4% fill / 3 / 1px 2 3px 4 stretch"]);
+test_valid_value("border-image", "1 / auto / 1px", ["1 / auto / 1px", "none 1 / auto / 1px stretch"]);
+test_valid_value("border-image", "1 2% 3 4% / 1px 2% 3 auto / 2", ["1 2% 3 4% / 1px 2% 3 auto / 2", "none 1 2% 3 4% / 1px 2% 3 auto / 2 2 2 2", "none 1 2% 3 4% / 1px 2% 3 auto / 2 stretch"]);
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-width-invalid.html b/css/css-backgrounds/parsing/border-image-width-invalid.html
new file mode 100644
index 0000000..f85e947
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-width-invalid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-width with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-width">
+<meta name="assert" content="border-image-width supports only the grammar '[ <length-percentage> | <number> | auto ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-image-width", "none");
+
+test_invalid_value("border-image-width", "-1px");
+test_invalid_value("border-image-width", "-2%");
+test_invalid_value("border-image-width", "-3");
+
+test_invalid_value("border-image-width", "1 2 3 4 5");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-image-width-valid.html b/css/css-backgrounds/parsing/border-image-width-valid.html
new file mode 100644
index 0000000..503f237
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-image-width-valid.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-image-width with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-image-width">
+<meta name="assert" content="border-image-width supports the full grammar '[ <length-percentage> | <number> | auto ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-image-width", "1px");
+test_valid_value("border-image-width", "2%");
+test_valid_value("border-image-width", "3");
+test_valid_value("border-image-width", "auto");
+
+test_valid_value("border-image-width", "1px 2% 3 auto");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-invalid.html b/css/css-backgrounds/parsing/border-invalid.html
new file mode 100644
index 0000000..f59f249
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-shorthands">
+<meta name="assert" content="border-width supports only the grammar '<line-width> || <line-style> || <color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border", "auto");
+test_invalid_value("border", "thin double green 1px");
+test_invalid_value("border", "-2em");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-radius-invalid.html b/css/css-backgrounds/parsing/border-radius-invalid.html
new file mode 100644
index 0000000..a8afbbe
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-radius-invalid.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-radius with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-radius">
+<meta name="assert" content="border-radius supports only the grammar '<length-percentage>{1,4} [ / <length-percentage>{1,4} ]?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-radius", "none");
+test_invalid_value("border-radius", "1px 2px 3px 4px 5px");
+test_invalid_value("border-radius", "-1px");
+test_invalid_value("border-radius", "1px -2px");
+test_invalid_value("border-radius", "1em /");
+test_invalid_value("border-radius", "1px / 2px / 3px");
+
+test_invalid_value("border-radius", "5em 1px 5em 2% 5em 3px 5em 4%");
+test_invalid_value("border-radius", "1px 5em 2% 5em 3px 5em 4% 5em");
+
+test_invalid_value("border-top-left-radius", "10px 20px 30px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-radius-valid.html b/css/css-backgrounds/parsing/border-radius-valid.html
new file mode 100644
index 0000000..cd6fa49
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-radius-valid.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-radius with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-radius">
+<meta name="assert" content="border-radius supports the full grammar '<length-percentage>{1,4} [ / <length-percentage>{1,4} ]?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-radius", "1px");
+test_valid_value("border-radius", "1px 2% 3px 4%");
+test_valid_value("border-radius", "5em / 1px 2% 3px 4%"); // Blink and Webkit fail by serializing as "5em 1px 5em 2% 5em 3px 5em 4%"
+test_valid_value("border-radius", "1px 2% 3px 4% / 5em"); // Blink and Webkit fail by serializing as "1px 5em 2% 5em 3px 5em 4% 5em"
+
+test_valid_value("border-top-left-radius", "10px");
+test_valid_value("border-top-right-radius", "20%");
+test_valid_value("border-bottom-right-radius", "30px 40%");
+test_valid_value("border-bottom-left-radius", "50% 60px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-style-invalid.html b/css/css-backgrounds/parsing/border-style-invalid.html
new file mode 100644
index 0000000..47e5422
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-style-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-style with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-style">
+<meta name="assert" content="border-style supports only the grammar '<line-style>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-style", "auto");
+test_invalid_value("border-style", "solid double groove ridge none");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-style-valid.html b/css/css-backgrounds/parsing/border-style-valid.html
new file mode 100644
index 0000000..0779854
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-style-valid.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-style with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-style">
+<meta name="assert" content="border-style supports the full grammar '<line-style>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-style", "none");
+test_valid_value("border-style", "none hidden dotted dashed");
+test_valid_value("border-style", "solid double groove ridge");
+test_valid_value("border-style", "inset outset");
+
+test_valid_value("border-top-style", "solid");
+test_valid_value("border-right-style", "double");
+test_valid_value("border-bottom-style", "groove");
+test_valid_value("border-left-style", "ridge");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-valid.html b/css/css-backgrounds/parsing/border-valid.html
new file mode 100644
index 0000000..5f29f6b
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-valid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-shorthands">
+<meta name="assert" content="border-width supports the full grammar '<line-width> || <line-style> || <color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border", "1px dotted red");
+test_valid_value("border", "green double thin", "thin double green");
+
+test_valid_value("border-top", "thin", ["thin", "thin none"]);
+test_valid_value("border-right", "double", ["double", "medium double"]);
+test_valid_value("border-bottom", "green", ["green", "medium none green"]);
+test_valid_value("border-left", "1px dotted red");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-width-invalid.html b/css/css-backgrounds/parsing/border-width-invalid.html
new file mode 100644
index 0000000..ff90837
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-width-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-width with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-width">
+<meta name="assert" content="border-width supports only the grammar '<line-width>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("border-width", "none");
+test_invalid_value("border-width", "thin medium thick medium thin");
+test_invalid_value("border-width", "-1px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/border-width-valid.html b/css/css-backgrounds/parsing/border-width-valid.html
new file mode 100644
index 0000000..b98a208
--- /dev/null
+++ b/css/css-backgrounds/parsing/border-width-valid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing border-width with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#border-width">
+<meta name="assert" content="border-width supports the full grammar '<line-width>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("border-width", "1em");
+test_valid_value("border-width", "2px thin medium thick");
+
+test_valid_value("border-top-width", "0px");
+test_valid_value("border-right-width", "thin");
+test_valid_value("border-bottom-width", "medium");
+test_valid_value("border-left-width", "thick");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/box-shadow-invalid.html b/css/css-backgrounds/parsing/box-shadow-invalid.html
new file mode 100644
index 0000000..fa00737
--- /dev/null
+++ b/css/css-backgrounds/parsing/box-shadow-invalid.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing box-shadow with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#box-shadow">
+<meta name="assert" content="box-shadow supports only the grammar 'none | <shadow>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("box-shadow", "auto");
+test_invalid_value("box-shadow", "1 2");
+test_invalid_value("box-shadow", "1% 2%");
+test_invalid_value("box-shadow", "1px calc(2px + 2%)");
+
+test_invalid_value("box-shadow", "red inset");
+test_invalid_value("box-shadow", "1px");
+
+test_invalid_value("box-shadow", "1px 2px 3px 4px 5px");
+test_invalid_value("box-shadow", "red 1px 2px blue");
+
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/box-shadow-valid.html b/css/css-backgrounds/parsing/box-shadow-valid.html
new file mode 100644
index 0000000..4ea289b
--- /dev/null
+++ b/css/css-backgrounds/parsing/box-shadow-valid.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Module Level 3: parsing box-shadow with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#box-shadow">
+<meta name="assert" content="box-shadow supports the full grammar 'none | <shadow>#'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("box-shadow", "none");
+test_valid_value("box-shadow", "1px 2px");
+test_valid_value("box-shadow", "red 1px 2px 3px -4px inset"); // Edge serializes as "inset 1px 2px 3px -4px red"
+test_valid_value("box-shadow", "inset 1px 2px red", "red 1px 2px inset");
+test_valid_value("box-shadow", "1px -2px inset, red -3px 4px"); // Edge serializes as "inset 1px -2px, -3px 4px red"
+test_valid_value("box-shadow", "inset 1px -2px, -3px 4px red", "1px -2px inset, red -3px 4px");
+</script>
+</body>
+</html>
diff --git a/css/css-backgrounds/parsing/resources/parsing-testcommon.js b/css/css-backgrounds/parsing/resources/parsing-testcommon.js
new file mode 100644
index 0000000..9427f53
--- /dev/null
+++ b/css/css-backgrounds/parsing/resources/parsing-testcommon.js
@@ -0,0 +1,39 @@
+'use strict';
+
+// serializedValue can be the expected serialization of value,
+// or an array of permitted serializations,
+// or omitted if value should serialize as value.
+function test_valid_value(property, value, serializedValue) {
+    if (arguments.length < 3)
+        serializedValue = value;
+
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_not_equals(div.style[property], "", "property should be set");
+
+        var div = document.createElement('div');
+        div.style[property] = value;
+        var readValue = div.style[property];
+        if (serializedValue instanceof Array)
+            assert_true(serializedValue.includes(readValue), "serialization should be sound");
+        else
+            assert_equals(readValue, serializedValue, "serialization should be canonical");
+
+        div.style[property] = readValue;
+        assert_equals(div.style[property], readValue, "serialization should round-trip");
+
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should set the property value");
+}
+
+function test_invalid_value(property, value) {
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_equals(div.style[property], "");
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should not set the property value");
+}
diff --git a/css/css-backgrounds/reference/background-rounded-image-clip.html b/css/css-backgrounds/reference/background-rounded-image-clip.html
new file mode 100644
index 0000000..2436bed
--- /dev/null
+++ b/css/css-backgrounds/reference/background-rounded-image-clip.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Corner Clipped Background Color</title>
+<style>
+    html {
+        background-color: green;
+    }
+    #a {
+        top: 20px;
+        left: 20px;
+        position: absolute;
+        width: 20px;
+        height: 20px;
+        background-color: black;
+    }
+</style>
+<div id="a"></div>
diff --git a/css/css-contain/OWNERS b/css/css-contain/OWNERS
new file mode 100644
index 0000000..9d556e0
--- /dev/null
+++ b/css/css-contain/OWNERS
@@ -0,0 +1,2 @@
+@tabatkins
+@frivoal
diff --git a/css/css-contain/contain-layout-001.html b/css/css-contain/contain-layout-001.html
new file mode 100644
index 0000000..ab3328e
--- /dev/null
+++ b/css/css-contain/contain-layout-001.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on non-atomic inlines</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to non atomic inlines">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+span {
+  contain: layout;
+  height: 100vh; /*If layout containment applies, the span becomes a BFC, height applies, and knocks SS off the page */
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><span>PA</span>SS</div>
diff --git a/css/css-contain/contain-layout-002.html b/css/css-contain/contain-layout-002.html
new file mode 100644
index 0000000..578893d
--- /dev/null
+++ b/css/css-contain/contain-layout-002.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment on ruby-base</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+  <meta name=assert content="layout containment does not apply to ruby-base">
+  <link rel="match" href="reference/contain-layout-002-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rb {
+  contain: layout;
+  display: ruby-base;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rb::before {
+  content: "X";
+  color: green;
+}
+rb::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rb></rb></ruby></div>
diff --git a/css/css-contain/contain-layout-003.html b/css/css-contain/contain-layout-003.html
new file mode 100644
index 0000000..b4d0672
--- /dev/null
+++ b/css/css-contain/contain-layout-003.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment on ruby-base-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+  <meta name=assert content="layout containment does not apply to ruby-base-container">
+  <link rel="match" href="reference/contain-layout-002-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rbc {
+  contain: layout;
+  display: ruby-base-container;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rbc::before {
+  content: "X";
+  color: green;
+}
+rbc::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rbc></rbc></ruby></div>
diff --git a/css/css-contain/contain-layout-004.html b/css/css-contain/contain-layout-004.html
new file mode 100644
index 0000000..0287b4a
--- /dev/null
+++ b/css/css-contain/contain-layout-004.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment on ruby-text-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+  <meta name=assert content="layout containment does not apply to ruby-text-container">
+  <link rel="match" href="reference/contain-layout-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rtc {
+  contain: layout;
+  display: ruby-text-container;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rtc::before {
+  content: "X";
+  color: green;
+}
+rtc::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rtc></rtc></ruby></div>
diff --git a/css/css-contain/contain-layout-005.html b/css/css-contain/contain-layout-005.html
new file mode 100644
index 0000000..3a6a3da
--- /dev/null
+++ b/css/css-contain/contain-layout-005.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment on ruby-text</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+  <meta name=assert content="layout containment does not apply to ruby-text">
+  <link rel="match" href="reference/contain-layout-005-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rt {
+  contain: layout;
+  display: ruby-text;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rt::before {
+  content: "X";
+  color: green;
+}
+rt::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rt></rt></ruby></div>
diff --git a/css/css-contain/contain-layout-breaks-001.html b/css/css-contain/contain-layout-breaks-001.html
new file mode 100644
index 0000000..3599989
--- /dev/null
+++ b/css/css-contain/contain-layout-breaks-001.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment and forced breaks</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="layout containment allows forced breaks.">
+  <link rel="match" href="reference/contain-style-breaks-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-layout">
+  <link rel=help href="https://drafts.csswg.org/css-break-3/#forced-break">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+  line-height: 1;
+  height: 4em;
+  column-fill: auto;
+}
+div > div:last-of-type {
+  break-before: column;
+}
+#test > div {
+  contain: layout;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<article id=test>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support forced break at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/contain-layout-breaks-002.html b/css/css-contain/contain-layout-breaks-002.html
new file mode 100644
index 0000000..d45b40c
--- /dev/null
+++ b/css/css-contain/contain-layout-breaks-002.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: layout containment and forced breaks</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="forced breaks within layout containment do not propagate to the parent.">
+  <link rel="match" href="reference/contain-layout-breaks-002-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-layout">
+  <link rel=help href="https://drafts.csswg.org/css-break-3/#forced-break">
+
+<style>
+article {
+  columns: 2 20px;
+  float: left;
+  height: 50px;
+  column-fill: auto;
+}
+div > div {
+  border-top: 20px solid orange;
+  break-before: column;
+}
+article > div {
+  border-top: 20px solid orange;
+  contain: layout;
+}
+</style>
+
+<p>Test passes if there are two orange boxes below.
+<article>
+  <div>
+    <div></div>
+  </div>
+</article>
diff --git a/css/css-contain/contain-paint-001.html b/css/css-contain/contain-paint-001.html
new file mode 100644
index 0000000..e0c17c4
--- /dev/null
+++ b/css/css-contain/contain-paint-001.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment use the padding edge</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment clips at the padding edge, not content edge, and takes corner clipping into account">
+  <link rel="match" href="reference/contain-paint-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+div {
+  width: 100px;
+  height: 100px;
+  background: blue;
+  padding: 50px;
+  border-radius: 100px;
+  border: white 50px solid;
+  contain: paint;
+}
+div::before {
+ content:"";
+ display: block;
+ background: green;
+ width: 100px;
+ height: 100px;
+}
+div::after {
+  content:"";
+  display: block;
+  background: red;
+  width: 50px;
+  height: 50px;
+  float: left;
+  margin-top: 36px;
+  margin-left: -86px;
+}
+</style>
+
+<p>Test passes if there is a green square in a rounded blue box, and no red.
+<div></div>
diff --git a/css/css-contain/contain-paint-002.html b/css/css-contain/contain-paint-002.html
new file mode 100644
index 0000000..6ea6478
--- /dev/null
+++ b/css/css-contain/contain-paint-002.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on non-atomic inlines</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to non atomic inlines">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+span {
+  contain: paint;
+  width: 0; /* Because if the test fails, the span may get blockified, and which would make wide enough to hold the PASS */
+}
+span::after {
+  content: "PASS";
+  position: absolute;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><span></span></div>
diff --git a/css/css-contain/contain-paint-003.html b/css/css-contain/contain-paint-003.html
new file mode 100644
index 0000000..9ab7270
--- /dev/null
+++ b/css/css-contain/contain-paint-003.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment applies to the principal box for tables</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment applies to the principal box, which is the table wrapper box for tables">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+table { contain: paint; }
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<table>
+  <caption>PASS</caption>
+</table>
diff --git a/css/css-contain/contain-paint-004.html b/css/css-contain/contain-paint-004.html
new file mode 100644
index 0000000..7567c92
--- /dev/null
+++ b/css/css-contain/contain-paint-004.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment applies to the principal box for list items</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment applies to the principal box, which for list items excludes the list marker">
+  <link rel="match" href="reference/contain-paint-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+li { contain: paint; }
+</style>
+
+<p>This test passes if you can see no number below.
+<ol>
+  <li>
+  <li>
+  <li>
+  <li>
+  <li>
+  <li>
+</ol>
diff --git a/css/css-contain/contain-paint-005.html b/css/css-contain/contain-paint-005.html
new file mode 100644
index 0000000..ce565e1
--- /dev/null
+++ b/css/css-contain/contain-paint-005.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on ruby-base</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to ruby-base">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rb {
+  contain: paint;
+  display: ruby-base;
+  width: 0; /* Because if the test fails, this may get blockified, and which could make wide enough to hold the PASS */
+}
+rb::after {
+  content: "PASS";
+  position: absolute;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rb></rb></ruby></div>
diff --git a/css/css-contain/contain-paint-006.html b/css/css-contain/contain-paint-006.html
new file mode 100644
index 0000000..54149fa
--- /dev/null
+++ b/css/css-contain/contain-paint-006.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on ruby-base-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to ruby-base-container">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rbc {
+  contain: paint;
+  display: ruby-base-container;
+  width: 0; /* Because if the test fails, this may get blockified, and which could make wide enough to hold the PASS */
+}
+rbc::after {
+  content: "PASS";
+  position: absolute;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rbc></rbc></ruby></div>
diff --git a/css/css-contain/contain-paint-007.html b/css/css-contain/contain-paint-007.html
new file mode 100644
index 0000000..fb76def
--- /dev/null
+++ b/css/css-contain/contain-paint-007.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on ruby-text-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to ruby-text-container">
+  <link rel="match" href="reference/contain-paint-007-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rtc {
+  contain: paint;
+  display: ruby-text-container;
+  width: 0; /* Because if the test fails, this may get blockified, and which could make wide enough to hold the PASS */
+  font-size: 1rem;
+}
+rtc::after {
+  content: "PASS";
+  position: absolute;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rtc></rtc></ruby></div>
diff --git a/css/css-contain/contain-paint-008.html b/css/css-contain/contain-paint-008.html
new file mode 100644
index 0000000..8dd628b
--- /dev/null
+++ b/css/css-contain/contain-paint-008.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on ruby-text</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="paint containment does not apply to ruby-text">
+  <link rel="match" href="reference/contain-paint-008-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-paint">
+
+<style>
+rt {
+  contain: paint;
+  display: ruby-text;
+  font-size: 1rem;
+}
+rt::after {
+  content: "PASS";
+
+  /* Doing the following instead of position:absolute to move it out into the area that would be clipped
+     because Firefox clips absolutely positioned content of rt even though it does not support
+     containment.
+     Since this technique works also, not need to trigger a false negative.
+   */
+  position: relative;
+  left: 4ch;
+  font-family: monospace;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rt></rt></ruby></div>
diff --git a/css/css-contain/contain-size-001.html b/css/css-contain/contain-size-001.html
new file mode 100644
index 0000000..f5491a7
--- /dev/null
+++ b/css/css-contain/contain-size-001.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment on non-atomic inlines</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="size containment does not apply to non atomic inlines">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-size">
+
+<style>
+div { overflow: hidden; }
+span { contain: size; }
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><span>PASS</span></div>
diff --git a/css/css-contain/contain-size-002.html b/css/css-contain/contain-size-002.html
new file mode 100644
index 0000000..fe30aa2
--- /dev/null
+++ b/css/css-contain/contain-size-002.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment on ruby-base</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="size containment does not to apply ruby-base, which is an internatl ruby element">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel=help href="https://drafts.csswg.org/css-display-3/#internal-ruby-element">
+
+<style>
+div {
+  overflow: hidden;
+  position: absolute;
+}
+rb {
+  contain: size;
+  display: ruby-base;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rb>PASS</rb></ruby></div>
diff --git a/css/css-contain/contain-size-003.html b/css/css-contain/contain-size-003.html
new file mode 100644
index 0000000..4f3cb96
--- /dev/null
+++ b/css/css-contain/contain-size-003.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment on ruby-base-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="size containment does not to apply ruby-base-container, which is an internatl ruby element">
+  <link rel="match" href="reference/contain-size-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel=help href="https://drafts.csswg.org/css-display-3/#internal-ruby-element">
+
+<style>
+div {
+  overflow: hidden;
+  position: absolute;
+}
+rbc {
+  contain: size;
+  display: ruby-base-container;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rbc>PASS</rbc></ruby></div>
diff --git a/css/css-contain/contain-size-004.html b/css/css-contain/contain-size-004.html
new file mode 100644
index 0000000..43691bc
--- /dev/null
+++ b/css/css-contain/contain-size-004.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment on ruby-text-container</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="size containment does not to apply ruby-text-container, which is an internatl ruby element">
+  <link rel="match" href="reference/contain-size-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel=help href="https://drafts.csswg.org/css-display-3/#internal-ruby-element">
+
+<style>
+div {
+  overflow: hidden;
+  position: absolute;
+}
+rtc {
+  contain: size;
+  display: ruby-text-container;
+  font-size: 1rem;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rtc>PASS</rtc></ruby></div>
diff --git a/css/css-contain/contain-size-005.html b/css/css-contain/contain-size-005.html
new file mode 100644
index 0000000..3687d4a
--- /dev/null
+++ b/css/css-contain/contain-size-005.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment on ruby-text</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="size containment does not to apply ruby-text, which is an internatl ruby element">
+  <link rel="match" href="reference/contain-size-005-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-size">
+  <link rel=help href="https://drafts.csswg.org/css-display-3/#internal-ruby-element">
+
+<style>
+div {
+  overflow: hidden;
+  position: absolute;
+}
+rt {
+  contain: size;
+  display: ruby-text;
+  font-size: 1rem;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rt>PASS</rt></ruby></div>
diff --git a/css/css-contain/contain-size-breaks-001.html b/css/css-contain/contain-size-breaks-001.html
new file mode 100644
index 0000000..b3284f5
--- /dev/null
+++ b/css/css-contain/contain-size-breaks-001.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: size containment and fragmentation</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+  <meta name=assert content="size containment makes element monolithic">
+  <link rel="match" href="reference/contain-size-breaks-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+  <link rel=help href="https://drafts.csswg.org/css-break-3/#monolithic">
+
+<style>
+article {
+  height: 2.5em;
+  column-gap: 0;
+  columns: 3 1em;
+  width: 3em;
+  column-fill: auto;
+
+  font-size: 40px;
+  font-family: ahem;
+  line-height: 1;
+}
+div {
+  contain: size;
+  height: 5em;
+  color: orange;
+}
+</style>
+
+<p>This test passes if there is an orange rectangle below. If the shape is not a rectangle, the test fails.
+
+<article>
+<div>A A A A A</div>
+</article>
+
+<!--
+If the element is monolythic, it may:
+* overflow and be a 1x5 em rectangle
+* be sliced and be 2 contigious 1x2.5 em rectangles, appearing as a single
+  2x2.5 em rectangle.
+Either way, it will always appear as a single rectangle.
+
+If the element is not monolithic, it will fit 2 As in the first column, 2 in
+the second column, and the fith will be overflowing the div, either into the
+thrid column, or into the bottom of the second one.
+Either way, this will not look like a rectangle.
+-->
diff --git a/css/css-contain/contain-style-breaks-001.html b/css/css-contain/contain-style-breaks-001.html
new file mode 100644
index 0000000..22ae3a5
--- /dev/null
+++ b/css/css-contain/contain-style-breaks-001.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and break-inside:avoid</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment is (no longer) supposed to have any effect on the break-inside property">
+  <link rel="match" href="reference/contain-style-breaks-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+}
+div {
+  page-break-inside: avoid;
+  break-inside: avoid;
+}
+#test > div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+  A<br>
+  A<br>
+  A<br>
+  A
+  <div>
+</article>
+<article id=test>
+  <div>
+  A<br>
+  A<br>
+  A<br>
+  A
+  <div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support the property at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/contain-style-breaks-002.html b/css/css-contain/contain-style-breaks-002.html
new file mode 100644
index 0000000..eabbf5b
--- /dev/null
+++ b/css/css-contain/contain-style-breaks-002.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and break-inside:avoid</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment is not (any longer) supposed to have any effect on the break-inside property. Same as -001, applying containment on the parent.">
+  <link rel="match" href="reference/contain-style-breaks-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+}
+div > div {
+  page-break-inside: avoid;
+  break-inside: avoid;
+}
+#test > div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+    <div>
+    A<br>
+    A<br>
+    A<br>
+    A
+    <div>
+  </div>
+</article>
+<article id=test>
+  <div>
+    <div>
+    A<br>
+    A<br>
+    A<br>
+    A
+    <div>
+  </div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support the property at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/contain-style-breaks-003.html b/css/css-contain/contain-style-breaks-003.html
new file mode 100644
index 0000000..9668c49
--- /dev/null
+++ b/css/css-contain/contain-style-breaks-003.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and break-inside:avoid</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment is not (any longer) supposed to have any effect on the break-inside property. Same as -001, applying break-inside on the parent.">
+  <link rel="match" href="reference/contain-style-breaks-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+}
+article > div {
+  page-break-inside: avoid;
+  break-inside: avoid;
+}
+#test div > div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+    <div>
+    A<br>
+    A<br>
+    A<br>
+    A
+    <div>
+  </div>
+</article>
+<article id=test>
+  <div>
+    <div>
+    A<br>
+    A<br>
+    A<br>
+    A
+    <div>
+  </div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support the property at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/contain-style-breaks-004.html b/css/css-contain/contain-style-breaks-004.html
new file mode 100644
index 0000000..c2998f9
--- /dev/null
+++ b/css/css-contain/contain-style-breaks-004.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and break-before</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment is not (any longer) supposed to have any effect on the break-before property.">
+  <link rel="match" href="reference/contain-style-breaks-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+  line-height: 1;
+  height: 4em;
+  column-fill: auto;
+}
+div > div:last-of-type {
+  break-before: column;
+}
+#test > div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<article id=test>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support the property at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/contain-style-breaks-005.html b/css/css-contain/contain-style-breaks-005.html
new file mode 100644
index 0000000..89fb829
--- /dev/null
+++ b/css/css-contain/contain-style-breaks-005.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and break-after</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment is not (any longer) supposed to have any effect on the break-after property.">
+  <link rel="match" href="reference/contain-style-breaks-004-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+  line-height: 1;
+  height: 4em;
+  column-fill: auto;
+}
+div > div:first-of-type {
+  break-after: column;
+}
+#test > div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article id=ref>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<article id=test>
+  <div>
+    <div>A<br>A</div>
+    <div>A<br>A</div>
+  </div>
+</article>
+<!--
+Having two blocks to avoid making browsers that don't support the property at all fail.
+Since containment is supposed to have no effect, failing such browsers would not be useful.
+-->
diff --git a/css/css-contain/counter-scoping-001.html b/css/css-contain/counter-scoping-001.html
new file mode 100644
index 0000000..92b075e
--- /dev/null
+++ b/css/css-contain/counter-scoping-001.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and counter-increment</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="counter-increment is scoped to the subtree and creates a new counter at the root of the subtree">
+  <link rel="match" href="reference/counter-scoping-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+div {
+  contain: style;
+   counter-increment: n;
+}
+div::before, div::after {
+  content: counters(n, '.') " ";
+}
+div::after {
+  counter-increment: n 2;
+}
+</style>
+
+<p>Test passes if the text below is "1 1.2" (not including the quotation marks).<p>
+<div></div>
+
diff --git a/css/css-contain/counter-scoping-002.html b/css/css-contain/counter-scoping-002.html
new file mode 100644
index 0000000..4dde5cd
--- /dev/null
+++ b/css/css-contain/counter-scoping-002.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and counter-set</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="counter-set is scoped to the subtree and creates a new counter at the root of the subtree">
+  <link rel="match" href="reference/counter-scoping-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+  <link rel=help href="https://drafts.csswg.org/css-lists-3/#propdef-counter-set">
+
+<style>
+div {
+  contain: style;
+  counter-set: n;
+}
+div::before, div::after {
+  content: counters(n, '.') " ";
+}
+div::after {
+  counter-set: n 2;
+}
+</style>
+
+<p>Test passes if the text below is "1 1.2" (not including the quotation marks).<p>
+<div></div>
+
diff --git a/css/css-contain/counter-scoping-003.html b/css/css-contain/counter-scoping-003.html
new file mode 100644
index 0000000..5d52401
--- /dev/null
+++ b/css/css-contain/counter-scoping-003.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and subtree root</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="When considering the effects of the scoped property on elements inside the subtree, the element at the base of the subtree is treated as if it was the root of the document">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+  <link rel="match" href="reference/counter-scoping-003-ref.html">
+
+<style>
+div {
+  contain: style;
+  counter-increment: c 123;
+}
+span::before {
+  content: counter(c);
+  counter-increment: c 1;
+}
+</style>
+
+<p>Test passes if the text below is "1 2" (not including the quotation marks).<p>
+<div>
+  <span></span>
+  <span></span>
+</div>
+
diff --git a/css/css-contain/quote-scoping-001.html b/css/css-contain/quote-scoping-001.html
new file mode 100644
index 0000000..cc6c4d4
--- /dev/null
+++ b/css/css-contain/quote-scoping-001.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and open-quote</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment cause the open-quote value of the content property are scoped to the element's subtree">
+  <link rel="match" href="reference/quote-scoping-001-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+
+div {
+  quotes: "A" "Z" "1" "9";
+}
+div::before, span::before {
+  content: open-quote;
+}
+div::after {
+  content: close-quote;
+}
+div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if the text below is "A1Z" (not including the quotation marks).<p>
+<div><span></span></div>
diff --git a/css/css-contain/quote-scoping-002.html b/css/css-contain/quote-scoping-002.html
new file mode 100644
index 0000000..6d699db
--- /dev/null
+++ b/css/css-contain/quote-scoping-002.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and close-quote</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment cause the close-quote value of the content property are scoped to the element's subtree">
+  <link rel="match" href="reference/quote-scoping-002-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+
+div {
+  quotes: "A" "Z" "1" "9";
+}
+div::before {
+  content: open-quote;
+}
+div::after, span::after {
+  content: close-quote;
+}
+div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if the text below is "A9Z" (not including the quotation marks).<p>
+<div><span></span></div>
diff --git a/css/css-contain/quote-scoping-003.html b/css/css-contain/quote-scoping-003.html
new file mode 100644
index 0000000..dceee59
--- /dev/null
+++ b/css/css-contain/quote-scoping-003.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and no-open-quote</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment cause the no-open-quote value of the content property are scoped to the element's subtree">
+  <link rel="match" href="reference/quote-scoping-003-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+
+div {
+  quotes: "A" "Z" "1" "9";
+}
+div::before{
+  content: open-quote;
+}
+
+span::before {
+  content: no-open-quote;
+}
+div::after {
+  content: close-quote;
+}
+div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if the text below is "AZ" (not including the quotation marks).<p>
+<div><span></span></div>
diff --git a/css/css-contain/quote-scoping-004.html b/css/css-contain/quote-scoping-004.html
new file mode 100644
index 0000000..8c75578
--- /dev/null
+++ b/css/css-contain/quote-scoping-004.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: style containment and no-close-quote</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+  <meta name=assert content="style containment cause the no-close-quote value of the content property are scoped to the element's subtree">
+  <link rel="match" href="reference/quote-scoping-003-ref.html">
+  <link rel=help href="https://drafts.csswg.org/css-contain/#containment-style">
+
+<style>
+
+div {
+  quotes: "A" "Z" "1" "9";
+}
+div::before{
+  content: open-quote;
+}
+
+span::after {
+  content: no-close-quote;
+}
+div::after {
+  content: close-quote;
+}
+div {
+  contain: style;
+}
+</style>
+
+<p>Test passes if the text below is "AZ" (not including the quotation marks).<p>
+<div><span></span></div>
diff --git a/css/css-contain/reference/contain-layout-002-ref.html b/css/css-contain/reference/contain-layout-002-ref.html
new file mode 100644
index 0000000..5fbeb3a
--- /dev/null
+++ b/css/css-contain/reference/contain-layout-002-ref.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test referene</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+
+<style>
+rb {
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rb::before {
+  content: "X";
+  color: green;
+}
+rb::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rb></rb></ruby></div>
diff --git a/css/css-contain/reference/contain-layout-004-ref.html b/css/css-contain/reference/contain-layout-004-ref.html
new file mode 100644
index 0000000..f3279e9
--- /dev/null
+++ b/css/css-contain/reference/contain-layout-004-ref.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+
+<style>
+rtc {
+  display: ruby-text-container;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rtc::before {
+  content: "X";
+  color: green;
+}
+rtc::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rtc></rtc></ruby></div>
diff --git a/css/css-contain/reference/contain-layout-005-ref.html b/css/css-contain/reference/contain-layout-005-ref.html
new file mode 100644
index 0000000..0b2792f
--- /dev/null
+++ b/css/css-contain/reference/contain-layout-005-ref.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+
+<style>
+rt {
+  display: ruby-text;
+  font-family: ahem;
+  font-size: 20px;
+  line-height: 1;
+}
+rt::before {
+  content: "X";
+  color: green;
+}
+rt::after {
+  content: "X";
+  color: white;
+  position: absolute;
+  top:0; left: 0;
+}
+</style>
+
+<p>This test passes if you can see a green box below.
+<div><ruby><rt></rt></ruby></div>
diff --git a/css/css-contain/reference/contain-layout-breaks-002-ref.html b/css/css-contain/reference/contain-layout-breaks-002-ref.html
new file mode 100644
index 0000000..9ecc3d2
--- /dev/null
+++ b/css/css-contain/reference/contain-layout-breaks-002-ref.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="">
+
+<style>
+article {
+  columns: 2 20px;
+  float: left;
+  height: 50px;
+  column-fill: auto;
+}
+article > div {
+  break-before: column;
+  border-top: 20px solid orange;
+}
+</style>
+
+<p>Test passes if there are two orange boxes below.
+<article>
+    <div></div>
+    <div></div>
+</article>
diff --git a/css/css-contain/reference/contain-paint-001-ref.html b/css/css-contain/reference/contain-paint-001-ref.html
new file mode 100644
index 0000000..1ed4eca
--- /dev/null
+++ b/css/css-contain/reference/contain-paint-001-ref.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment use the padding edge</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<style>
+div {
+  width: 100px;
+  height: 100px;
+  background: blue;
+  padding: 50px;
+  border-radius: 100px;
+  border: white 50px solid;
+  overflow: hidden
+}
+div::before {
+  content:"";
+  display: block;
+  background: green;
+  width: 100px;
+  height: 100px;
+}
+div::after {
+  content:"";
+  display: block;
+  background: red;
+  width: 50px;
+  height: 50px;
+  float: left;
+  margin-top: 36px;
+  margin-left: -86px;
+}
+</style>
+
+<p>Test passes if there is a green square in a rounded blue box, and no red.
+<div></div>
diff --git a/css/css-contain/reference/contain-paint-004-ref.html b/css/css-contain/reference/contain-paint-004-ref.html
new file mode 100644
index 0000000..c03b4c8
--- /dev/null
+++ b/css/css-contain/reference/contain-paint-004-ref.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>This test passes if you can see no number below.
diff --git a/css/css-contain/reference/contain-paint-007-ref.html b/css/css-contain/reference/contain-paint-007-ref.html
new file mode 100644
index 0000000..3048d55
--- /dev/null
+++ b/css/css-contain/reference/contain-paint-007-ref.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<style>
+rtc {
+  display: ruby-text-container;
+  font-size: 1rem;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rtc>PASS</rtc></ruby></div>
diff --git a/css/css-contain/reference/contain-paint-008-ref.html b/css/css-contain/reference/contain-paint-008-ref.html
new file mode 100644
index 0000000..50c3bd7
--- /dev/null
+++ b/css/css-contain/reference/contain-paint-008-ref.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test: paint containment on ruby-text</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<style>
+rt {
+  display: ruby-text;
+  font-size: 1rem;
+  position: relative;
+  left: 4ch;
+  font-family: monospace;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rt>PASS</rt></ruby></div>
diff --git a/css/css-contain/reference/contain-size-001-ref.html b/css/css-contain/reference/contain-size-001-ref.html
new file mode 100644
index 0000000..9b669db
--- /dev/null
+++ b/css/css-contain/reference/contain-size-001-ref.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>This test passes if you can see the word PASS below.
+<div>PASS</div>
diff --git a/css/css-contain/reference/contain-size-004-ref.html b/css/css-contain/reference/contain-size-004-ref.html
new file mode 100644
index 0000000..3048d55
--- /dev/null
+++ b/css/css-contain/reference/contain-size-004-ref.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<style>
+rtc {
+  display: ruby-text-container;
+  font-size: 1rem;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rtc>PASS</rtc></ruby></div>
diff --git a/css/css-contain/reference/contain-size-005-ref.html b/css/css-contain/reference/contain-size-005-ref.html
new file mode 100644
index 0000000..001fc90
--- /dev/null
+++ b/css/css-contain/reference/contain-size-005-ref.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<style>
+div { overflow: hidden; }
+rt {
+  display: ruby-text;
+  font-size: 1rem;
+}
+</style>
+
+<p>This test passes if you can see the word PASS below.
+<div><ruby><rt>PASS</rt></ruby></div>
diff --git a/css/css-contain/reference/contain-size-breaks-001-ref.html b/css/css-contain/reference/contain-size-breaks-001-ref.html
new file mode 100644
index 0000000..6fa1e47
--- /dev/null
+++ b/css/css-contain/reference/contain-size-breaks-001-ref.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+  <meta name=flags content="ahem">
+
+<style>
+article {
+  height: 2.5em;
+  column-gap: 0;
+  columns: 3 1em;
+  width: 3em;
+  column-fill: auto;
+
+  font-size: 40px;
+  font-family: ahem;
+  line-height: 1;
+}
+div {
+  background: orange;
+  padding-top: 5em;
+}
+</style>
+
+<p>This test passes if there is an orange rectangle below. If the shape is not a rectangle, the test fails.
+
+<article>
+<div></div>
+</article>
diff --git a/css/css-contain/reference/contain-style-breaks-001-ref.html b/css/css-contain/reference/contain-style-breaks-001-ref.html
new file mode 100644
index 0000000..2c0b758
--- /dev/null
+++ b/css/css-contain/reference/contain-style-breaks-001-ref.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+}
+div {
+  page-break-inside: avoid;
+  break-inside: avoid;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article>
+  <div>
+  A<br>
+  A<br>
+  A<br>
+  A
+  <div>
+</article>
+<article>
+  <div>
+  A<br>
+  A<br>
+  A<br>
+  A
+  <div>
+</article>
diff --git a/css/css-contain/reference/contain-style-breaks-004-ref.html b/css/css-contain/reference/contain-style-breaks-004-ref.html
new file mode 100644
index 0000000..eaf4d56
--- /dev/null
+++ b/css/css-contain/reference/contain-style-breaks-004-ref.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+<style>
+article {
+  columns: 2 1ch;
+  column-gap: 0;
+  float: left;
+  font-family: monospace;
+  margin-right: 3em;
+  line-height: 1;
+  height: 4em;
+  column-fill: auto;
+}
+div:last-of-type {
+  break-before: column;
+}
+</style>
+
+<p>Test passes if there are two identical blocks “A” letters below.
+<article>
+  <div>A<br>A</div>
+  <div>A<br>A</div>
+</article>
+<article>
+  <div>A<br>A</div>
+  <div>A<br>A</div>
+</article>
diff --git a/css/css-contain/reference/counter-scoping-001-ref.html b/css/css-contain/reference/counter-scoping-001-ref.html
new file mode 100644
index 0000000..2ebbb9a
--- /dev/null
+++ b/css/css-contain/reference/counter-scoping-001-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>Test passes if the text below is "1 1.2" (not including the quotation marks).<p>
+<div>1 1.2</div>
+
diff --git a/css/css-contain/reference/counter-scoping-003-ref.html b/css/css-contain/reference/counter-scoping-003-ref.html
new file mode 100644
index 0000000..49e7b85
--- /dev/null
+++ b/css/css-contain/reference/counter-scoping-003-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>Test passes if the text below is "1 2" (not including the quotation marks).<p>
+<div>1 2</div>
+
diff --git a/css/css-contain/reference/quote-scoping-001-ref.html b/css/css-contain/reference/quote-scoping-001-ref.html
new file mode 100644
index 0000000..0b0e9c4
--- /dev/null
+++ b/css/css-contain/reference/quote-scoping-001-ref.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+<p>Test passes if the text below is "A1Z" (not including the quotation marks).<p>
+<div>A1Z</div>
+
diff --git a/css/css-contain/reference/quote-scoping-002-ref.html b/css/css-contain/reference/quote-scoping-002-ref.html
new file mode 100644
index 0000000..8b846e2
--- /dev/null
+++ b/css/css-contain/reference/quote-scoping-002-ref.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>Test passes if the text below is "A9Z" (not including the quotation marks).<p>
+<div>A9Z</span></div>
diff --git a/css/css-contain/reference/quote-scoping-003-ref.html b/css/css-contain/reference/quote-scoping-003-ref.html
new file mode 100644
index 0000000..2bc8d67
--- /dev/null
+++ b/css/css-contain/reference/quote-scoping-003-ref.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html lang=en>
+  <meta charset=utf-8>
+  <title>CSS-contain test reference</title>
+  <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net">
+
+<p>Test passes if the text below is "AZ" (not including the quotation marks).<p>
+<div>AZ</div>
diff --git a/css/css-display/display-contents-details-001-ref.html b/css/css-display/display-contents-details-001-ref.html
new file mode 100644
index 0000000..fc078b8
--- /dev/null
+++ b/css/css-display/display-contents-details-001-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Test Reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<details>
+  <div>
+    <summary>summary</summary>
+    details
+  </div>
+</details>
diff --git a/css/css-display/display-contents-details-001.html b/css/css-display/display-contents-details-001.html
new file mode 100644
index 0000000..b1cefa6
--- /dev/null
+++ b/css/css-display/display-contents-details-001.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Test: display: contents under a details element doesn't prevent content from getting suppressed</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-display-3/#valdef-display-contents">
+<link rel="match" href="display-contents-details-001-ref.html">
+<details>
+  <div style="display:contents">
+    <summary>summary</summary>
+    details
+  </div>
+</details>
diff --git a/css/css-display/display-contents-suppression-dynamic-001-ref.html b/css/css-display/display-contents-suppression-dynamic-001-ref.html
new file mode 100644
index 0000000..07a5663
--- /dev/null
+++ b/css/css-display/display-contents-suppression-dynamic-001-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Test Reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<p>Test passes if you see nothing below.</p>
diff --git a/css/css-display/display-contents-suppression-dynamic-001.html b/css/css-display/display-contents-suppression-dynamic-001.html
new file mode 100644
index 0000000..5007e1f
--- /dev/null
+++ b/css/css-display/display-contents-suppression-dynamic-001.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Test: display: contents unboxing works in presence of dynamic changes to the tree.</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-display-3/#valdef-display-contents">
+<link rel="help" href="https://drafts.csswg.org/css-display-3/#unbox">
+<link rel="match" href="display-contents-suppression-dynamic-001-ref.html">
+<p>Test passes if you see nothing below.</p>
+<textarea style="display: contents">
+  FAIL
+</textarea>
+<script>
+  let textarea = document.querySelector("textarea");
+  textarea.offsetTop;
+  textarea.appendChild(document.createTextNode("FAIL"));
+</script>
diff --git a/css/css-flexbox/flexbox_first-letter.html b/css/css-flexbox/flexbox_first-letter.html
new file mode 100644
index 0000000..dbbb03d
--- /dev/null
+++ b/css/css-flexbox/flexbox_first-letter.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>flexbox | first-letter</title>
+<meta name="assert" content="This test is ensures that flexbox placement does not apply to ::first-letter psuedo-content">
+<link rel="author" title="Vince Falconi" href="vince.falconi@gmail.com">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#placement">
+<style>
+  div { display: flex; }
+  div::first-letter { order: 2 }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div>Triceratops</div>
+
+<script>
+  test(function() {
+    let order = getComputedStyle(document.querySelector('div'), '::first-letter').order;
+    assert_not_equals(order, '2');
+  });
+</script>
diff --git a/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-items-invalid.html b/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-items-invalid.html
index d1f29e3..6ea876b 100644
--- a/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-items-invalid.html
+++ b/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-items-invalid.html
@@ -17,6 +17,6 @@
     var body = document.body;
 
     assert_equals(getComputedStyle(body).getPropertyValue("align-items"),
-	          "stretch");
+	          "normal");
 });
 </script>
diff --git a/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-self-invalid.html b/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-self-invalid.html
index 0059bc2..5312ea8 100644
--- a/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-self-invalid.html
+++ b/css/css-flexbox/getcomputedstyle/flexbox_computedstyle_align-self-invalid.html
@@ -17,6 +17,6 @@
     var body = document.body;
 
     assert_equals(getComputedStyle(body).getPropertyValue("align-self"),
-	          "stretch");
+	          "auto");
 });
 </script>
diff --git a/css/css-flexbox/percentage-size-subitems-001.html b/css/css-flexbox/percentage-size-subitems-001.html
new file mode 100644
index 0000000..70f3953
--- /dev/null
+++ b/css/css-flexbox/percentage-size-subitems-001.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Flexbox Test: Percentage size on child of a flex item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#flex-items">
+<link rel="match" href="reference/percentage-size-subitems-001-ref.html">
+<meta name="assert" content="Checks that flex items children resolve properly their percentage sizes, even when the flex item has margin, border, padding and scrollbar.">
+<style>
+.flex {
+  display: inline-flex;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  flex: 1;
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+}
+
+.subitem {
+  width: 100%;
+  height: 100%;
+  background: yellow;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
diff --git a/css/css-flexbox/reference/percentage-size-subitems-001-ref.html b/css/css-flexbox/reference/percentage-size-subitems-001-ref.html
new file mode 100644
index 0000000..b9883c5f
--- /dev/null
+++ b/css/css-flexbox/reference/percentage-size-subitems-001-ref.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Refttest Reference: Percentage size on child of a flex item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.flex {
+  display: inline-block;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+  width: calc(100% - 6px);
+  height: calc(100% - 4px);
+  box-sizing: border-box;
+}
+
+.subitem {
+  width: 100%;
+  height: 100%;
+  background: yellow;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item horizontalTB">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalLR">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="flex">
+  <div class="item verticalRL">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
diff --git a/css/css-fonts/font-display/font-display-preload-ref.html b/css/css-fonts/font-display/font-display-preload-ref.html
new file mode 100644
index 0000000..e3ba8ca
--- /dev/null
+++ b/css/css-fonts/font-display/font-display-preload-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<title>font-display should work for preloaded fonts</title>
+<style>
+  .arial {
+    height: 20px;
+    font-family: Arial;
+  }
+</style>
+<div id="container">
+  <div class="arial"></div>
+  <div class="arial"></div>
+  <div class="arial">Swap</div>
+  <div class="arial">Fallback</div>
+  <div class="arial">Optional</div>
+</div>
+</html>
diff --git a/css/css-fonts/font-display/font-display-preload.html b/css/css-fonts/font-display/font-display-preload.html
new file mode 100644
index 0000000..46f392f
--- /dev/null
+++ b/css/css-fonts/font-display/font-display-preload.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>font-display should work for preloaded fonts</title>
+<link id="link" href="/fonts/Ahem.ttf?pipe=trickle(d5)" as="font" type="font/ttf" crossorigin>
+<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#font-display-desc">
+<link rel="match" href="font-display-preload-ref.html">
+<style>
+  @font-face {
+    font-family: 'Auto';
+    src: url('/fonts/Ahem.ttf?pipe=trickle(d5)');
+    font-display: auto;
+  }
+  @font-face {
+    font-family: 'Block';
+    src: url('/fonts/Ahem.ttf?pipe=trickle(d5)');
+    font-display: block;
+  }
+  @font-face {
+    font-family: 'Swap';
+    src: url('/fonts/Ahem.ttf?pipe=trickle(d5)');
+    font-display: swap;
+  }
+  @font-face {
+    font-family: 'Fallback';
+    src: url('/fonts/Ahem.ttf?pipe=trickle(d5)');
+    font-display: fallback;
+  }
+  @font-face {
+    font-family: 'Optional';
+    src: url('/fonts/Ahem.ttf?pipe=trickle(d5)');
+    font-display: optional;
+  }
+</style>
+<div id="container" hidden>
+  <div style="height: 20px; font-family: 'Auto', Arial">Auto</div>
+  <div style="height: 20px; font-family: 'Block', Arial">Block</div>
+  <div style="height: 20px; font-family: 'Swap', Arial">Swap</div>
+  <div style="height: 20px; font-family: 'Fallback', Arial">Fallback</div>
+  <div style="height: 20px; font-family: 'Optional', Arial">Optional</div>
+</div>
+<script>
+    window.onload = () => {
+        document.getElementById('link').rel = 'preload';
+        document.getElementById('container').hidden = false;
+        const timeoutMsec = 200; // Between the short limit and the long limit
+        setTimeout(() => {
+            document.documentElement.classList.remove("reftest-wait");
+        }, timeoutMsec);
+    };
+</script>
+</html>
diff --git a/css/css-fonts/font-display/font-display.html b/css/css-fonts/font-display/font-display.html
index 9336377..7b17460 100644
--- a/css/css-fonts/font-display/font-display.html
+++ b/css/css-fonts/font-display/font-display.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html class="reftest-wait">
 <title>Test for font-display @font-face descriptor</title>
+<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#font-display-desc">
 <link rel="match" href="font-display-ref.html">
 <style>
 .hidden { display: none; }
diff --git a/css/css-fonts/font-face-unicode-range-2-ref.html b/css/css-fonts/font-face-unicode-range-2-ref.html
new file mode 100644
index 0000000..ff53ab6
--- /dev/null
+++ b/css/css-fonts/font-face-unicode-range-2-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reference: font-face unicode-range</title>
+<style>
+  @font-face {
+    font-family: base;
+    src: url(support/fonts/LigatureSymbolsWithSpaces.woff);
+  }
+  @font-face {
+    font-family: swoosh;
+    src: url(support/fonts/Rochester.otf);
+  }
+  .ref {
+    font-family: base;
+  }
+  .ref .amp {
+	  font-family: swoosh;
+  }
+  div {
+	  font-size: 5em;
+  }
+</style>
+<body>
+  <p>Test passes if the two lines look the same, with just the ampersand in italic</p>
+  <div class="ref">This <span class="amp">&amp;</span> That</div>
+  <div class="ref">This <span class="amp">&amp;</span> That</div>
+</body>
diff --git a/css/css-fonts/font-face-unicode-range-2.html b/css/css-fonts/font-face-unicode-range-2.html
index 9be431b..83eab75 100644
--- a/css/css-fonts/font-face-unicode-range-2.html
+++ b/css/css-fonts/font-face-unicode-range-2.html
@@ -3,6 +3,7 @@
 <title>CSS Test: font-face unicode-range</title>
 <link rel="author" title="Chris Lilley" href="chris@w3.org">
 <link rel="help" href="http://www.w3.org/TR/css-fonts-3/#unicode-range-desc">
+<link rel="match" href="font-face-unicode-range-2-ref.html">
 <meta name="flags" content="">
 <meta name="assert" content="Check that font-face unicode-range restrics use of glyphs outside that range">
 <style>
diff --git a/css/css-fonts/font-size-adjust-001-ref.html b/css/css-fonts/font-size-adjust-001-ref.html
new file mode 100644
index 0000000..b608c09
--- /dev/null
+++ b/css/css-fonts/font-size-adjust-001-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Fonts Reference: font-size-adjust - greater than aspect value of font</title>
+<style>
+  div {
+    position: absolute;
+    font: 40px/40px Ahem;
+    color: orange;
+  }
+  #test {
+    color: blue;
+    font-size: 45px;
+  }
+</style>
+<body>
+  <p>Test passes if the size of the blue rectangle is greater than the orange rectangle.</p>
+  <div id="test">FillerText</div>
+  <div>FillerText</div>
+</body>
diff --git a/css/css-fonts/font-size-adjust-001.html b/css/css-fonts/font-size-adjust-001.html
index d2d1241..b4b1c75 100644
--- a/css/css-fonts/font-size-adjust-001.html
+++ b/css/css-fonts/font-size-adjust-001.html
@@ -9,8 +9,9 @@
 is greater than the aspect value of font">
 <style>
   div {
-    font-family: Ahem;
-    font-size: 40px;
+    position: absolute;
+    font: 40px/40px Ahem;
+    color: orange;
   }
   #test {
     color: blue;
@@ -18,7 +19,7 @@
   }
 </style>
 <body>
-  <p>Test passes if the size of blue rectangle is greater than the black rectangle.</p>
-  <div>FillerText</div>
+  <p>Test passes if the size of the blue rectangle is greater than the orange rectangle.</p>
   <div id="test">FillerText</div>
+  <div>FillerText</div>
 </body>
diff --git a/css/css-fonts/font-size-adjust-002-ref.html b/css/css-fonts/font-size-adjust-002-ref.html
new file mode 100644
index 0000000..175802c
--- /dev/null
+++ b/css/css-fonts/font-size-adjust-002-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Fonts Reference: font-size-adjust - less than aspect value of font</title>
+<style>
+  div {
+    position: absolute;
+    font: 40px/40px Ahem;
+    color: blue;
+  }
+  #test {
+    color: orange;
+    font-size: 10px;
+  }
+</style>
+<body>
+  <p>Test passes if the size of the blue rectangle is greater than the orange rectangle.</p>
+  <div>FillerText</div>
+  <div id="test">FillerText</div>
+</body>
diff --git a/css/css-fonts/font-size-adjust-002.html b/css/css-fonts/font-size-adjust-002.html
index d157f13..e1e4768 100644
--- a/css/css-fonts/font-size-adjust-002.html
+++ b/css/css-fonts/font-size-adjust-002.html
@@ -4,21 +4,23 @@
 <link rel="author" title="Intel" href="http://www.intel.com/">
 <link rel="author" title="Shiyou Tan" href="mailto:shiyoux.tan@intel.com">
 <link rel="help" title="3.6 Relative sizing: the font-size-adjust property" href="http://www.w3.org/TR/css-fonts-3/#font-size-adjust-prop">
+<link rel="match" href="font-size-adjust-002-ref.html">
 <meta name="flags" content="Ahem">
 <meta name="assert" content="Test checks that the actual font size is less than the specified size when the value of font-size-adjust
 is less than the aspect value of font">
 <style>
   div {
-    font-family: Ahem;
-    font-size: 40px;
+    position: absolute;
+    font: 40px/40px Ahem;
+    color: blue;
   }
   #test {
-    color: blue;
+    color: orange;
     font-size-adjust: 0.2;
   }
 </style>
 <body>
-  <p>Test passes if the size of blue rectangle is less than the black rectangle.</p>
+  <p>Test passes if the size of the blue rectangle is greater than the orange rectangle.</p>
   <div>FillerText</div>
   <div id="test">FillerText</div>
 </body>
diff --git a/css/css-fonts/variations/font-parse-numeric-stretch-style-weight.html b/css/css-fonts/variations/font-parse-numeric-stretch-style-weight.html
index 5152424..fed4aa5 100644
--- a/css/css-fonts/variations/font-parse-numeric-stretch-style-weight.html
+++ b/css/css-fonts/variations/font-parse-numeric-stretch-style-weight.html
@@ -20,16 +20,18 @@
     '850.3',
     'calc(100 + 300)',
     'calc(0.2 + 205.5)',
+    'calc(0 - 100)',
+    'calc(200 + 801)',
   ],
-  'stretch': ['51%', '199%', 'calc(10% + 20%)'],
+  'stretch': ['51%', '199%', 'calc(10% + 20%)', '0%'],
   'style' : [ 'normal', 'italic', 'oblique', 'oblique 50deg', 'oblique -90deg', 'oblique 90deg',
-              'oblique calc(30deg + 20deg)' ]
+              'oblique calc(90deg + 20deg)', 'oblique calc(30deg + 20deg)' ]
 };
 
 var styleInvalidTests = {
-  'weight': ['100 400', 'calc(0 - 100)', 'calc(200 + 801)'],
-  'stretch': ['100% 110%', '0%', '100% 150%', 'calc(1 + 10%)'],
-  'style' : [ 'normal 10deg', 'italic 10deg', 'oblique -91deg', 'oblique 91deg', 'oblique calc(90deg + 20deg)']
+  'weight': ['100 400'],
+  'stretch': ['100% 110%', '100% 150%', 'calc(1 + 10%)'],
+  'style' : [ 'normal 10deg', 'italic 10deg', 'oblique -91deg', 'oblique 91deg' ]
 };
 
 function testParseStyle() {
@@ -61,9 +63,12 @@
   'weight': [
     ['100', '100'], ['700', '700'], ['900', '900'], ['bold', 'bold'],
     ['normal', 'normal'], ['100 400', '100 400'], ['100 101.5', '100 101.5'],
-    ['999.8 999.9', '999.8 999.9']
+    ['999.8 999.9', '999.8 999.9'],
+    [ '500 400', '500 400']
   ],
   'stretch': [
+    ['0%', '0%'],
+    ['calc(0% - 10%)', 'calc(-10%)' ],
     ['100%', '100%'],
     ['110%', '110%'],
     ['111.5%', '111.5%'],
@@ -88,7 +93,6 @@
     '0.9',
     '-100 200',
     '100 -200',
-    '500 400',
     '100 1001',
     '1001',
     '1000.5',
@@ -97,7 +101,7 @@
     'a b c',
   ],
   'stretch': [
-    '-0.5%', '-1%', '0%', 'calc(0% - 10%)', '60% 70% 80%', 'a%', 'a b c', '0.1',
+    '-0.5%', '-1%', '60% 70% 80%', 'a%', 'a b c', '0.1',
     '-60% 80%', 'ultra-expannnned', '50% 0'
   ],
   'style' : [ 'oblique 100deg', 'oblique italic', 'oblique -91deg', 'oblique 0',
diff --git a/css/css-fonts/variations/font-shorthand.html b/css/css-fonts/variations/font-shorthand.html
index 1b15957..c0f1f54 100644
--- a/css/css-fonts/variations/font-shorthand.html
+++ b/css/css-fonts/variations/font-shorthand.html
@@ -26,10 +26,10 @@
 
             // font-style
             { value: "oblique 45deg 24px Arial",                            isValid:true,   expectedStyle: "oblique 45deg",  message: "'oblique' with positive angle" },
-            { value: "oblique -45deg 24px Arial",                           isValid:true,   expectedStyle: "oblique -45deg", message: "'oblique' with hegative angle" },
+            { value: "oblique -45deg 24px Arial",                           isValid:true,   expectedStyle: "oblique -45deg", message: "'oblique' with negative angle" },
             { value: "oblique 24px Arial",                                  isValid:true,   expectedStyle: "oblique",        message: "'oblique' without slant angle" },
-            { value: "oblique 100deg 24px Arial",                           isValid:false,                                   message: "'oblique' with negative angle, value out of range" },
-            { value: "oblique -100deg 24px Arial",                          isValid:false,                                   message: "'oblique' with positive angle, value out of range" },
+            { value: "oblique 100deg 24px Arial",                           isValid:false,                                   message: "'oblique' with positive angle, value out of range" },
+            { value: "oblique -100deg 24px Arial",                          isValid:false,                                   message: "'oblique' with negative angle, value out of range" },
 
             // font-weight and font-style combined
             { value: "oblique 50 24px Arial",                               isValid:true,   expectedStyle: "oblique",        expectedWeight:"50",    message: "'oblique' followed by valid small weight" },
@@ -43,7 +43,7 @@
             { value: "oblique calc(900 + 300) 24px Arial",                  isValid:true,  expectedStyle: "oblique",         expectedWeight:"1000",  message: "'oblique' followed by a to-be-clamped calc() weight" },
             { value: "calc(200 + 300) oblique 24px Arial",                  isValid:true,  expectedStyle: "oblique",         expectedWeight:"500",   message: "calc() weight folowed by 'oblique'" },
             { value: "calc(200 + 300) oblique 45deg 24px Arial",            isValid:true,  expectedStyle: "oblique 45deg",   expectedWeight:"500",   message: "calc() weight folowed by 'oblique' and slant angle" },
-            { value: "calc(900 + 300) oblique 45deg 24px Arial",            isValid:true,  expectedStyle: "oblique",         expectedWeight:"1000",  message: "To-be-clamped calc() weight folowed by 'oblique' and slant angle" },
+            { value: "calc(900 + 300) oblique 45deg 24px Arial",            isValid:true,  expectedStyle: "oblique 45deg",   expectedWeight:"1000",  message: "To-be-clamped calc() weight folowed by 'oblique' and slant angle" },
         ];
 
         testFontShorthand.forEach(function (testCase) {
diff --git a/css/css-fonts/variations/font-variation-settings-parsing.html b/css/css-fonts/variations/font-variation-settings-parsing.html
index 93c6ec0..3d6bc5f 100644
--- a/css/css-fonts/variations/font-variation-settings-parsing.html
+++ b/css/css-fonts/variations/font-variation-settings-parsing.html
@@ -11,10 +11,10 @@
     <script>
 
         var valueParserTests = [
-            { value: "'wght' 1000, '9 ~A' -45",           expectedComputedValue: "\"9 ~A\" -45, \"wght\" 1000",    isValid: true,  message: "Axis tag with valid non-letter ascii characters" },
+            { value: "'wght' 1000, '9 ~A' -45",           expectedComputedValue: "\"wght\" 1000, \"9 ~A\" -45",    isValid: true,  message: "Axis tag with valid non-letter ascii characters" },
             { value: "'\u001Fbdc' 123",                   expectedComputedValue: "",                               isValid: false, message: "Invalid character below allowed range (first char)"},
             { value: "'abc\u007F' 123",                   expectedComputedValue: "",                               isValid: false, message: "Invalid character above allowed range (mid char)"},
-            { value: "'wght' 1e3, 'slnt' -450.0e-1 ",     expectedComputedValue: "\"slnt\" -45, \"wght\" 1000",    isValid: true,  message: "Axis values in scientific form are valid" },
+            { value: "'wght' 1e3, 'slnt' -450.0e-1 ",     expectedComputedValue: "\"wght\" 1000, \"slnt\" -45",    isValid: true,  message: "Axis values in scientific form are valid" },
             { value: "normal",                            expectedComputedValue: "normal",                         isValid: true,  message: "'normal' value is valid" },
             { value: "'a' 1234",                          expectedComputedValue: "",                               isValid: false, message: "Tag with less than 4 characters is invalid"},
             { value: "'abcde' 1234",                      expectedComputedValue: "",                               isValid: false, message: "Tag with more than 4 characters is invalid"},
@@ -35,6 +35,8 @@
         valueParserTests.forEach(function (testCase) {
             test(() => {
                 var element = document.getElementById("value-parser-test");
+                // Reset to empty in order for testing to continue in case the next test would not parse as valid
+                element.style.fontVariationSettings = "";
                 element.style.fontVariationSettings = testCase.value;
                 var computed = window.getComputedStyle(element).fontVariationSettings;
                 if (testCase.isValid) {
diff --git a/css/css-grid/grid-items/percentage-size-replaced-subitems-001-ref.html b/css/css-grid/grid-items/percentage-size-replaced-subitems-001-ref.html
new file mode 100644
index 0000000..2c21c8c
--- /dev/null
+++ b/css/css-grid/grid-items/percentage-size-replaced-subitems-001-ref.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Refttest Reference: Percentage size on child of a grid item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.grid {
+  display: inline-block;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+  width: calc(100% - 6px);
+  height: calc(100% - 4px);
+  box-sizing: border-box;
+}
+
+img {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img class="verticalLR" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img class="horizontalTB" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img class="horizontalTB" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
diff --git a/css/css-grid/grid-items/percentage-size-replaced-subitems-001.html b/css/css-grid/grid-items/percentage-size-replaced-subitems-001.html
new file mode 100644
index 0000000..190277a
--- /dev/null
+++ b/css/css-grid/grid-items/percentage-size-replaced-subitems-001.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Grid Test: Percentage size on replaced child of a grid item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#grid-items">
+<link rel="match" href="percentage-size-replaced-subitems-001-ref.html">
+<meta name="assert" content="Checks that grid items replaced children resolve properly their percentage sizes, even when the grid item has margin, border, padding and scrollbar.">
+<style>
+.grid {
+  display: inline-grid;
+  border: solid 5px black;
+  grid: 100px / 150px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+}
+
+img {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img class="verticalLR" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img class="horizontalTB" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img class="horizontalTB" src="support/100x100-green.png" />
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <img class="verticalRL" src="support/100x100-green.png" />
+  </div>
+</div>
diff --git a/css/css-grid/grid-items/percentage-size-subitems-001-ref.html b/css/css-grid/grid-items/percentage-size-subitems-001-ref.html
new file mode 100644
index 0000000..939504e
--- /dev/null
+++ b/css/css-grid/grid-items/percentage-size-subitems-001-ref.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Refttest Reference: Percentage size on child of a grid item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.grid {
+  display: inline-block;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+  width: calc(100% - 6px);
+  height: calc(100% - 4px);
+  box-sizing: border-box;
+}
+
+.subitem {
+  width: 100%;
+  height: 100%;
+  background: yellow;
+  font: 20px/1 Ahem;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
diff --git a/css/css-grid/grid-items/percentage-size-subitems-001.html b/css/css-grid/grid-items/percentage-size-subitems-001.html
new file mode 100644
index 0000000..ad00990
--- /dev/null
+++ b/css/css-grid/grid-items/percentage-size-subitems-001.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Grid Test: Percentage size on child of a grid item with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#grid-items">
+<link rel="match" href="percentage-size-subitems-001-ref.html">
+<meta name="assert" content="Checks that grid items children resolve properly their percentage sizes, even when the grid item has margin, border, padding and scrollbar.">
+<style>
+.grid {
+  display: inline-grid;
+  border: solid 5px black;
+  grid: 100px / 150px;
+  margin: 10px;
+  vertical-align: top;
+}
+
+.item {
+  overflow: scroll;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  background: cyan;
+}
+
+.subitem {
+  width: 100%;
+  height: 100%;
+  background: yellow;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if in the different examples you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item horizontalTB">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalLR">
+    <div class="subitem verticalRL"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem horizontalTB"></div>
+  </div>
+</div>
+
+<div class="grid">
+  <div class="item verticalRL">
+    <div class="subitem verticalLR"></div>
+  </div>
+</div>
diff --git a/css/css-images/parsing/gradient-position-invalid.html b/css/css-images/parsing/gradient-position-invalid.html
new file mode 100644
index 0000000..e53486e
--- /dev/null
+++ b/css/css-images/parsing/gradient-position-invalid.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing gradients with invalid position values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-values-4/#typedef-position">
+<meta name="assert" content="gradient positions support only the '<position>' grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// The following were supported in an earlier version of the spec.
+// https://github.com/w3c/csswg-drafts/issues/2140
+// Deprecated in Blink with support to be removed in M68, around July 2018.
+test_invalid_value("background-image", "radial-gradient(at center left 1px, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at center top 2px, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at right 3% center, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at left 4px top, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at right top 5px, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at bottom 6% center, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at bottom 7% left, red, blue)");
+test_invalid_value("background-image", "radial-gradient(at bottom right 8%, red, blue)");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/gradient-position-valid.html b/css/css-images/parsing/gradient-position-valid.html
new file mode 100644
index 0000000..6eae484
--- /dev/null
+++ b/css/css-images/parsing/gradient-position-valid.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing gradients with valid position values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-values-4/#typedef-position">
+<meta name="assert" content="gradient positions support the full '<position>' grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Where two values are shown, the first serialization is being used by Blink/Firefox/WebKit and the second by Edge.
+
+test_valid_value("background-image", "radial-gradient(at 10%, red, blue)", ["radial-gradient(at 10% center, red, blue)", "radial-gradient(at 10%, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at 20% 30px, red, blue)");
+test_valid_value("background-image", "radial-gradient(at 30px center, red, blue)", ["radial-gradient(at 30px center, red, blue)", "radial-gradient(at 30px, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at 40px top, red, blue)");
+test_valid_value("background-image", "radial-gradient(at bottom 10% right 20%, red, blue)", "radial-gradient(at right 20% bottom 10%, red, blue)");
+test_valid_value("background-image", "radial-gradient(at bottom right, red, blue)", "radial-gradient(at right bottom, red, blue)");
+test_valid_value("background-image", "radial-gradient(at center, red, blue)", ["radial-gradient(at center center, red, blue)", "radial-gradient(at center, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at center 50px, red, blue)");
+test_valid_value("background-image", "radial-gradient(at center bottom, red, blue)", ["radial-gradient(at center bottom, red, blue)", "radial-gradient(at bottom, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at center center, red, blue)", ["radial-gradient(at center center, red, blue)", "radial-gradient(at center, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at center left, red, blue)", ["radial-gradient(at left center, red, blue)", "radial-gradient(at left, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at left, red, blue)", ["radial-gradient(at left center, red, blue)", "radial-gradient(at left, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at left bottom, red, blue)");
+test_valid_value("background-image", "radial-gradient(at left center, red, blue)", ["radial-gradient(at left center, red, blue)", "radial-gradient(at left, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at right 40%, red, blue)");
+test_valid_value("background-image", "radial-gradient(at right 30% top 60px, red, blue)");
+test_valid_value("background-image", "radial-gradient(at top, red, blue)", ["radial-gradient(at center top, red, blue)", "radial-gradient(at top, red, blue)"]);
+test_valid_value("background-image", "radial-gradient(at top center, red, blue)", ["radial-gradient(at center top, red, blue)", "radial-gradient(at top, red, blue)"]);
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-orientation-invalid.html b/css/css-images/parsing/image-orientation-invalid.html
new file mode 100644
index 0000000..b36d9b7
--- /dev/null
+++ b/css/css-images/parsing/image-orientation-invalid.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing image-orientation with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-orientation">
+<meta name="assert" content="image-orientation supports only the grammar 'from-image | <angle> | [ <angle>? flip ]'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("image-orientation", "auto");
+test_invalid_value("image-orientation", "none");
+test_invalid_value("image-orientation", "0");
+test_invalid_value("image-orientation", "0 flip");
+test_invalid_value("image-orientation", "0deg from-image");
+test_invalid_value("image-orientation", "flip 0deg");
+test_invalid_value("image-orientation", "flip from-image");
+test_invalid_value("image-orientation", "from-image 0deg");
+test_invalid_value("image-orientation", "from-image flip");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-orientation-valid.html b/css/css-images/parsing/image-orientation-valid.html
new file mode 100644
index 0000000..1ed7548
--- /dev/null
+++ b/css/css-images/parsing/image-orientation-valid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing image-orientation with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-orientation">
+<meta name="assert" content="image-orientation supports the full grammar 'from-image | <angle> | [ <angle>? flip ]'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("image-orientation", "from-image");
+test_valid_value("image-orientation", "30deg");
+test_valid_value("image-orientation", "flip", "0deg flip"); // "0deg flip" in Firefox.
+test_valid_value("image-orientation", "-1.25turn flip");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-rendering-invalid.html b/css/css-images/parsing/image-rendering-invalid.html
new file mode 100644
index 0000000..84b0727
--- /dev/null
+++ b/css/css-images/parsing/image-rendering-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing image-rendering with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-rendering">
+<meta name="assert" content="image-rendering supports only the grammar 'auto | smooth | high-quality | crisp-edges | pixelated'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("image-rendering", "none");
+test_invalid_value("image-rendering", "high-quality crisp-edges");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-rendering-valid.html b/css/css-images/parsing/image-rendering-valid.html
new file mode 100644
index 0000000..80ea1c4
--- /dev/null
+++ b/css/css-images/parsing/image-rendering-valid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing image-rendering with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-rendering">
+<meta name="assert" content="image-rendering supports the full grammar 'auto | smooth | high-quality | crisp-edges | pixelated'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("image-rendering", "auto");
+test_valid_value("image-rendering", "smooth");
+test_valid_value("image-rendering", "high-quality");
+test_valid_value("image-rendering", "crisp-edges");
+test_valid_value("image-rendering", "pixelated");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-resolution-invalid.html b/css/css-images/parsing/image-resolution-invalid.html
new file mode 100644
index 0000000..332ad15
--- /dev/null
+++ b/css/css-images/parsing/image-resolution-invalid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 4: parsing image-resolution with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#propdef-image-resolution">
+<meta name="assert" content="image-resolution supports only the grammar '[ from-image || <resolution> ] && snap?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("image-resolution", "auto");
+test_invalid_value("image-resolution", "100%");
+test_invalid_value("image-resolution", "2");
+test_invalid_value("image-resolution", "3dpi snap from-image");
+test_invalid_value("image-resolution", "from-image snap 4dppx");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/image-resolution-valid.html b/css/css-images/parsing/image-resolution-valid.html
new file mode 100644
index 0000000..9317902
--- /dev/null
+++ b/css/css-images/parsing/image-resolution-valid.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 4: parsing image-resolution with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#propdef-image-resolution">
+<meta name="assert" content="image-resolution supports the full grammar '[ from-image || <resolution> ] && snap?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+
+
+
+test_valid_value("image-resolution", "1dpi");
+test_valid_value("image-resolution", "2dpcm from-image");
+test_valid_value("image-resolution", "3dppx from-image snap");
+test_valid_value("image-resolution", "4dpi snap");
+test_valid_value("image-resolution", "from-image");
+test_valid_value("image-resolution", "from-image 5dpcm");
+test_valid_value("image-resolution", "from-image 6dppx snap");
+test_valid_value("image-resolution", "from-image snap");
+test_valid_value("image-resolution", "snap 7.5dpi");
+test_valid_value("image-resolution", "snap -8dpcm from-image");
+test_valid_value("image-resolution", "snap from-image");
+test_valid_value("image-resolution", "snap from-image 0dppx");
+
+
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/object-fit-invalid.html b/css/css-images/parsing/object-fit-invalid.html
new file mode 100644
index 0000000..e4a298b
--- /dev/null
+++ b/css/css-images/parsing/object-fit-invalid.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 4: parsing object-fit with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#propdef-object-fit">
+<meta name="assert" content="object-fit supports only the grammar 'fill | none | [contain | cover] || scale-down'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("object-fit", "auto");
+test_invalid_value("object-fit", "contain cover");
+test_invalid_value("object-fit", "fill scale-down");
+test_invalid_value("object-fit", "contain fill");
+test_invalid_value("object-fit", "cover none");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/object-fit-valid.html b/css/css-images/parsing/object-fit-valid.html
new file mode 100644
index 0000000..0f73cfe
--- /dev/null
+++ b/css/css-images/parsing/object-fit-valid.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 4: parsing object-fit with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#propdef-object-fit">
+<meta name="assert" content="object-fit supports the full grammar 'fill | none | [contain | cover] || scale-down'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("object-fit", "contain");
+test_valid_value("object-fit", "contain scale-down");
+test_valid_value("object-fit", "cover");
+test_valid_value("object-fit", "cover scale-down");
+test_valid_value("object-fit", "fill");
+test_valid_value("object-fit", "none");
+test_valid_value("object-fit", "scale-down");
+test_valid_value("object-fit", "scale-down contain");
+test_valid_value("object-fit", "scale-down cover");
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/object-position-invalid.html b/css/css-images/parsing/object-position-invalid.html
new file mode 100644
index 0000000..87de7b9
--- /dev/null
+++ b/css/css-images/parsing/object-position-invalid.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing object-position with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-object-position">
+<meta name="assert" content="object-position supports only the '<position>' grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("object-position", "auto");
+test_invalid_value("object-position", "1px 2px 3px");
+test_invalid_value("object-position", "left right");
+test_invalid_value("object-position", "bottom 10% top 20%");
+
+// The following were supported in an earlier version of the spec.
+// https://github.com/w3c/csswg-drafts/issues/2140
+// Deprecated in Blink with support to be removed in M68, around July 2018.
+test_invalid_value("object-position", "center left 1px");
+test_invalid_value("object-position", "center top 2px");
+test_invalid_value("object-position", "right 3% center");
+test_invalid_value("object-position", "left 4px top");
+test_invalid_value("object-position", "right top 5px");
+test_invalid_value("object-position", "bottom 6% center");
+test_invalid_value("object-position", "bottom 7% left");
+test_invalid_value("object-position", "bottom right 8%");
+
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/object-position-valid.html b/css/css-images/parsing/object-position-valid.html
new file mode 100644
index 0000000..f78f67e
--- /dev/null
+++ b/css/css-images/parsing/object-position-valid.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: parsing object-position with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-object-position">
+<meta name="assert" content="object-position supports the full '<position>' grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// First serialization is being returned by Blink/Firefox/WebKit, second by Edge.
+test_valid_value("object-position", "10%", ["10% center", "10% 50%"]);
+test_valid_value("object-position", "right 30% top 60px"); // "calc(70%) 60px" in Edge.
+test_valid_value("object-position", "20% 30px");
+test_valid_value("object-position", "30px center", ["30px center", "30px 50%"]);
+test_valid_value("object-position", "40px top", ["40px top", "40px 0%"]);
+test_valid_value("object-position", "bottom 10% right 20%", "right 20% bottom 10%"); // "calc(80%) calc(90%)" in Edge.
+test_valid_value("object-position", "bottom right", ["right bottom", "100% 100%"]);
+test_valid_value("object-position", "center 50px", ["center 50px", "50% 50px"]);
+test_valid_value("object-position", "center bottom", ["center bottom", "50% 100%"]);
+test_valid_value("object-position", "center left", ["left center", "0% 50%"]);
+test_valid_value("object-position", "left", ["left center", "0% 50%"]);
+test_valid_value("object-position", "left bottom", ["left bottom", "0% 100%"]);
+test_valid_value("object-position", "left center", ["left center", "0% 50%"]);
+test_valid_value("object-position", "right 40%", ["right 40%", "100% 40%"]);
+test_valid_value("object-position", "top", ["center top", "50% 0%"]);
+test_valid_value("object-position", "top center", ["center top", "50% 0%"]);
+
+// ["center center"] in Blink and Firefox, "center" in WebKit, "50% 50%" in Edge.
+test_valid_value("object-position", "center", ["center center", "center", "50% 50%"]);
+test_valid_value("object-position", "center center", ["center center", "center", "50% 50%"]);
+</script>
+</body>
+</html>
diff --git a/css/css-images/parsing/resources/parsing-testcommon.js b/css/css-images/parsing/resources/parsing-testcommon.js
new file mode 100644
index 0000000..9427f53
--- /dev/null
+++ b/css/css-images/parsing/resources/parsing-testcommon.js
@@ -0,0 +1,39 @@
+'use strict';
+
+// serializedValue can be the expected serialization of value,
+// or an array of permitted serializations,
+// or omitted if value should serialize as value.
+function test_valid_value(property, value, serializedValue) {
+    if (arguments.length < 3)
+        serializedValue = value;
+
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_not_equals(div.style[property], "", "property should be set");
+
+        var div = document.createElement('div');
+        div.style[property] = value;
+        var readValue = div.style[property];
+        if (serializedValue instanceof Array)
+            assert_true(serializedValue.includes(readValue), "serialization should be sound");
+        else
+            assert_equals(readValue, serializedValue, "serialization should be canonical");
+
+        div.style[property] = readValue;
+        assert_equals(div.style[property], readValue, "serialization should round-trip");
+
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should set the property value");
+}
+
+function test_invalid_value(property, value) {
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_equals(div.style[property], "");
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should not set the property value");
+}
diff --git a/css/css-multicol/zero-column-width-computed-style.html b/css/css-multicol/zero-column-width-computed-style.html
new file mode 100644
index 0000000..46d876f
--- /dev/null
+++ b/css/css-multicol/zero-column-width-computed-style.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>column-width:0</title>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-multicol/#cw" title="3.1. column-width">
+<div id="longhand" style="column-width:0;"></div>
+<div id="shorthand" style="columns:0;"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+  assert_equals(getComputedStyle(longhand).columnWidth, "0px");
+  assert_equals(getComputedStyle(shorthand).columnWidth, "0px");
+}, "column-width:0 is a valid CSS declaration");
+</script>
diff --git a/css/css-multicol/zero-column-width-layout.html b/css/css-multicol/zero-column-width-layout.html
new file mode 100644
index 0000000..aaae421
--- /dev/null
+++ b/css/css-multicol/zero-column-width-layout.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>column-width:0</title>
+<meta name="assert" content="column-width:0 is valid as specified and computed value, but its used value may never be less than 1px">
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-multicol/#cw" title="3.1. column-width">
+<link rel="match" href="../reference/ref-filled-green-100px-square-only.html">
+<p>Test passes if there is a filled green square.</p>
+<div style="float:left; width:50px; height:100px; column-width:0; column-gap:0;">
+  <div style="height:5000px; background:green;"></div>
+</div>
+<div style="float:left; width:50px; height:100px; columns:0; column-gap:0;">
+  <div style="height:5000px; background:green;"></div>
+</div>
diff --git a/css/css-overflow/overflow-shorthand-001.html b/css/css-overflow/overflow-shorthand-001.html
new file mode 100644
index 0000000..f425636
--- /dev/null
+++ b/css/css-overflow/overflow-shorthand-001.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Overflow Test: Overflow longhand accepts two values</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez <emilio@crisal.io>">
+<link rel="help" href="https://drafts.csswg.org/css-overflow/#propdef-overflow">
+<div id="test-div"></div>
+<script>
+let div = document.getElementById("test-div");
+function testOverflowShorthand(x, y) {
+  test(function() {
+    div.style.overflowX = x;
+    div.style.overflowY = y;
+
+    let expectedX = getComputedStyle(div).overflowX;
+    let expectedY = getComputedStyle(div).overflowY;
+    let expectedComputedSerialization = expectedX == expectedY ? expectedX : `${expectedX} ${expectedY}`;
+    let expectedSpecifiedSerialization = x == y ? x : `${x} ${y}`;
+
+    assert_equals(div.style.overflow, expectedSpecifiedSerialization);
+    assert_equals(getComputedStyle(div).overflow, expectedComputedSerialization);
+
+    div.style.overflowX = "";
+    div.style.overflowY = "";
+    assert_equals(div.style.overflow, "");
+
+    div.style.overflow = `${x} ${y}`;
+    assert_equals(div.style.overflow, expectedSpecifiedSerialization);
+    assert_equals(div.style.overflowX, x);
+    assert_equals(div.style.overflowY, y);
+    assert_equals(getComputedStyle(div).overflow, expectedComputedSerialization);
+    assert_equals(getComputedStyle(div).overflowX, expectedX);
+    assert_equals(getComputedStyle(div).overflowY, expectedY);
+  }, `overflow: ${x} ${y} works`);
+}
+
+let OVERFLOW_VALUES = [ "auto", "hidden", "scroll", "visible" ];
+for (let x of OVERFLOW_VALUES)
+  for (let y of OVERFLOW_VALUES)
+    testOverflowShorthand(x, y);
+</script>
diff --git a/css/css-position/OWNERS b/css/css-position/OWNERS
new file mode 100644
index 0000000..c323201
--- /dev/null
+++ b/css/css-position/OWNERS
@@ -0,0 +1 @@
+@atanassov
diff --git a/css/css-position/hypothetical-box-scroll-parent-ref.html b/css/css-position/hypothetical-box-scroll-parent-ref.html
new file mode 100644
index 0000000..86956ad
--- /dev/null
+++ b/css/css-position/hypothetical-box-scroll-parent-ref.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- Our target div should be placed as if it were a child of the overflowing --
+  -- thing; this is simplest to accomplish by just having the overflowing thing --
+  -- absolutely positioned so the target div is placed as if it were not there -->
+<div style="overflow: auto; height: 100px; width: 200px; position: absolute">
+  <div style="width: 400px; height: 10px"></div>
+</div>
+<div id="target">Modified text</div>
+<script>
+  document.querySelector("div").scrollLeft = 1000;
+</script>
diff --git a/css/css-position/hypothetical-box-scroll-parent.html b/css/css-position/hypothetical-box-scroll-parent.html
new file mode 100644
index 0000000..e342e8c
--- /dev/null
+++ b/css/css-position/hypothetical-box-scroll-parent.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<link rel=match href=hypothetical-box-scroll-parent-ref.html>
+<link rel="help"
+      href="https://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-width">
+<div style="overflow: auto; height: 100px; width: 200px">
+  <div id="target" style="position: absolute">Original text</div>
+  <div style="width: 400px; height: 10px"></div>
+</div>
+<script>
+  // Scroll the parent.
+  document.querySelector("div").scrollLeft = 1000;
+
+  // Now force relayout of the abs pos div.
+  document.getElementById("target").textContent = "Modified text";
+</script>
diff --git a/css/css-position/hypothetical-box-scroll-viewport-ref.html b/css/css-position/hypothetical-box-scroll-viewport-ref.html
new file mode 100644
index 0000000..368da5f
--- /dev/null
+++ b/css/css-position/hypothetical-box-scroll-viewport-ref.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<meta charset=utf-8>
+<div>Modified text</div>
+<div style="width: 200vw; height: 10px"></div>
+<script>
+  window.scrollTo(window.innerWidth * 2, 0);
+</script>
diff --git a/css/css-position/hypothetical-box-scroll-viewport.html b/css/css-position/hypothetical-box-scroll-viewport.html
new file mode 100644
index 0000000..9ccb822
--- /dev/null
+++ b/css/css-position/hypothetical-box-scroll-viewport.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<link rel=match href=hypothetical-box-scroll-viewport-ref.html>
+<link rel="help"
+      href="https://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-width">
+<div style="position: absolute">Original text</div>
+<div style="width: 200vw; height: 10px"></div>
+<script>
+  // Scroll the viewport.
+  window.scrollTo(window.innerWidth * 2, 0);
+
+  // Now force relayout of the abs pos div.
+  document.querySelector("div").textContent = "Modified text";
+</script>
diff --git a/css/css-shapes/basic-shape-circle-ellipse-serialization.html b/css/css-shapes/basic-shape-circle-ellipse-serialization.html
index 9c82039..5e4842d 100644
--- a/css/css-shapes/basic-shape-circle-ellipse-serialization.html
+++ b/css/css-shapes/basic-shape-circle-ellipse-serialization.html
@@ -35,8 +35,8 @@
             "ellipse(closest-side farthest-side at 50% 50%)");
 
 
-checkEquals("circle(at right 5px top)", "circle(at right 5px top 0%)");
-checkEquals("ellipse(at right 10px top)", "ellipse(at right 10px top 0%)");
+checkEquals("circle(at top 0% right 5px)", "circle(at right 5px top 0%)");
+checkEquals("ellipse(at top 0% right 10px)", "ellipse(at right 10px top 0%)");
 // Remove defaults like closest-side
 checkEquals("circle(closest-side at center)",
             "circle(at 50% 50%)");
diff --git a/css/css-shapes/parsing/shape-outside-invalid-position.html b/css/css-shapes/parsing/shape-outside-invalid-position.html
index 4815402..7d2b64b 100644
--- a/css/css-shapes/parsing/shape-outside-invalid-position.html
+++ b/css/css-shapes/parsing/shape-outside-invalid-position.html
@@ -14,15 +14,16 @@
 <script>
 // The following were supported in an earlier version of the spec.
 // https://github.com/w3c/csswg-drafts/issues/2140
-// Deprecated in Blink with support to be removed in M68, around July 2018.
 test_invalid_value("shape-outside", "circle(at center left 1px)");
 test_invalid_value("shape-outside", "circle(at center top 2px)");
 test_invalid_value("shape-outside", "circle(at right 3% center)");
 test_invalid_value("shape-outside", "circle(at left 4px top)");
-test_invalid_value("shape-outside", "circle(at right top 5px)");
-test_invalid_value("shape-outside", "circle(at bottom 6% center)");
-test_invalid_value("shape-outside", "circle(at bottom 7% left)");
-test_invalid_value("shape-outside", "circle(at bottom right 8%)");
+test_invalid_value("shape-outside", "circle(at right 5px top)");
+test_invalid_value("shape-outside", "ellipse(at right top 5px)");
+test_invalid_value("shape-outside", "ellipse(at bottom 6% center)");
+test_invalid_value("shape-outside", "ellipse(at bottom 7% left)");
+test_invalid_value("shape-outside", "ellipse(at bottom right 8%)");
+test_invalid_value("shape-outside", "ellipse(at right 10px top)");
 </script>
 </body>
 </html>
diff --git a/css/css-shapes/shape-outside/shape-image/shape-image-010.html b/css/css-shapes/shape-outside/shape-image/shape-image-010.html
index aedab21..df993be 100644
--- a/css/css-shapes/shape-outside/shape-image/shape-image-010.html
+++ b/css/css-shapes/shape-outside/shape-image/shape-image-010.html
@@ -40,7 +40,7 @@
             height: 100px;
             shape-outside: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAALpJREFUeNrs0UEVABAURcHP5pcRSxpR9FHGhhycuQ3emxI9TnxQ7pxttfH6jhoCIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACIiBABASIgAARECACAsQFQAQEiIAAERAgAgJEQAQEiIAAEZDPuwAAAP//AwCf+AWUylJrCQAAAABJRU5ErkJggg==);
             shape-margin: 5%;
-            shape-image-threshold: 0.25;
+            shape-image-threshold: 0.7;
         }
         .blue {
             width: 2px;
diff --git a/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-027.html b/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-027.html
index 2141c12..b00f6c4 100644
--- a/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-027.html
+++ b/css/css-shapes/shape-outside/supported-shapes/circle/shape-outside-circle-027.html
@@ -36,7 +36,7 @@
         padding: 10px;
         border: 10px solid transparent;
         shape-margin: 15px;
-        shape-outside: margin-box circle(35% at 85px 75px);
+        shape-outside: margin-box circle(60px);
     }
     #line {
         position: absolute;
diff --git a/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-015.html b/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-015.html
index 3502f8b..1a434a0 100644
--- a/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-015.html
+++ b/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-015.html
@@ -34,7 +34,7 @@
         margin: 10px;
         padding: 10px;
         border: 10px solid transparent;
-        shape-outside: padding-box ellipse(closest-side at 75px 80px);
+        shape-outside: padding-box ellipse(closest-side closest-side at 75px 80px);
     }
     #line {
         position: absolute;
diff --git a/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-017.html b/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-017.html
index 3cf6089..fd9b816 100644
--- a/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-017.html
+++ b/css/css-shapes/shape-outside/supported-shapes/ellipse/shape-outside-ellipse-017.html
@@ -36,7 +36,7 @@
         padding: 10px;
         border: 10px solid transparent;
         shape-margin: 10px;
-        shape-outside: content-box ellipse(farthest-side);
+        shape-outside: content-box ellipse(farthest-side closest-side);
     }
     #line {
         position: absolute;
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-010.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-010.html
index 72147aa..49627c8 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-010.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-010.html
@@ -19,8 +19,7 @@
         #test-container {
             width: 200px;
             height: 200px;
-            font-family: Ahem;
-            font-size: 25px;
+            font: 25px/1 Ahem;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-011.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-011.html
index 9a65184..96d2421 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-011.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-011.html
@@ -21,6 +21,7 @@
             height: 200px;
             font-family: Ahem;
             font-size: 25px;
+            line-height: 1;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-012.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-012.html
index d7988c2..b9dbe52 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-012.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-012.html
@@ -22,6 +22,7 @@
             height: 200px;
             font-family: Ahem;
             font-size: 25px;
+            line-height: 1;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-013.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-013.html
index 03e4baa..d49dd70 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-013.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-013.html
@@ -22,6 +22,7 @@
             height: 200px;
             font-family: Ahem;
             font-size: 25px;
+            line-height: 1;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-014.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-014.html
index bb791a5..30604af 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-014.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-014.html
@@ -22,6 +22,7 @@
             height: 200px;
             font-family: Ahem;
             font-size: 25px;
+            line-height: 1;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-015.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-015.html
index b2e0e06..da54fb2 100644
--- a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-015.html
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-015.html
@@ -22,6 +22,7 @@
             height: 200px;
             font-family: Ahem;
             font-size: 25px;
+            line-height: 1;
             background-color: red;
             color: green;
         }
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-028.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-028.html
new file mode 100644
index 0000000..2e0d9ab
--- /dev/null
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-028.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>CSS Test: left float, inset, px units</title>
+        <link rel="author" title="Brad Werth" href="mailto:bwerth@mozilla.com">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#funcdef-inset">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#shape-outside-property">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#shape-margin-property">
+        <link rel="match" href="reference/shape-outside-inset-010-ref.html"/>
+        <meta name="flags" content="ahem" />
+        <meta name="assert" content="The test verfies that text flows around a
+                                     right float with a shape-outside defined as
+                                     an inset rounded rectangle in px units with
+                                     a shape-margin.">
+    </head>
+    <style>
+        #container {
+            position: relative;
+            margin-left: 25px;
+        }
+        #test-container {
+            width: 200px;
+            height: 200px;
+            font: 25px/1 Ahem;
+            background-color: red;
+            color: green;
+            text-align: right;
+        }
+        #test-shape {
+            float: right;
+            width: 200px;
+            height: 200px;
+            background-color: green;
+            shape-margin: 10px;
+            shape-outside: inset(60px 10px 60px 110px round 20px);
+        }
+        #static-shape {
+            position: absolute;
+            left: 100px;
+            width: 100px;
+            height: 100px;
+            top: 50px;
+            background-color: green;
+        }
+    </style>
+    <body>
+        <p>The test passes if there is a green square and no red.</p>
+        <div id="container">
+            <div id="test-container">
+                <div id="test-shape"></div>
+                XXXXXXXX XXXXXXXX XXXX XXXX XXXX XXXX XXXXXXXX XXXXXXXX
+            </div>
+            <div id="static-shape"></div>
+        </div>
+    </body>
+</html>
diff --git a/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-029.html b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-029.html
new file mode 100644
index 0000000..fe30d43
--- /dev/null
+++ b/css/css-shapes/shape-outside/supported-shapes/inset/shape-outside-inset-029.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>CSS Test: left float, inset, px units</title>
+        <link rel="author" title="Brad Werth" href="mailto:bwerth@mozilla.com">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#funcdef-inset">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#shape-outside-property">
+        <link rel="help" href="http://www.w3.org/TR/css-shapes-1/#shape-margin-property">
+        <link rel="match" href="reference/shape-outside-inset-010-ref.html"/>
+        <meta name="flags" content="ahem" />
+        <meta name="assert" content="The test verfies that text flows around a
+                                     right float with a shape-outside defined as
+                                     an inset irregular elliptically rounded
+                                     rectangle in px units with a shape-margin.">
+    </head>
+    <style>
+        #container {
+            position: relative;
+            margin-left: 25px;
+        }
+        #test-container {
+            width: 200px;
+            height: 200px;
+            font: 25px/1 Ahem;
+            background-color: red;
+            color: green;
+            text-align: right;
+        }
+        #test-shape {
+            float: right;
+            width: 200px;
+            height: 200px;
+            background-color: green;
+            shape-margin: 10px;
+            shape-outside: inset(60px 10px 60px 110px round 70px 0px 0px 10px / 10px 0px 0px 20px);
+        }
+        #static-shape {
+            position: absolute;
+            left: 100px;
+            width: 100px;
+            height: 100px;
+            top: 50px;
+            background-color: green;
+        }
+    </style>
+    <body>
+        <p>The test passes if there is a green square and no red.</p>
+        <div id="container">
+            <div id="test-container">
+                <div id="test-shape"></div>
+                XXXXXXXX XXXXXXXX XXXX XXXX XXXX XXXX XXXXXXXX XXXXXXXX
+            </div>
+            <div id="static-shape"></div>
+        </div>
+    </body>
+</html>
diff --git a/css/css-shapes/shape-outside/values/support/parsing-utils.js b/css/css-shapes/shape-outside/values/support/parsing-utils.js
index cc174ec..68b0c1b 100644
--- a/css/css-shapes/shape-outside/values/support/parsing-utils.js
+++ b/css/css-shapes/shape-outside/values/support/parsing-utils.js
@@ -534,65 +534,6 @@
     ["60% center", "60% 50%"],
     ["60u1 center", "60u1 50%"],
 
-////// [ keyword | keyword percent ], [ keyword | keyword length ] x 5 keywords
-    ["center top 50%", "50% 50%"],
-    ["center top 50u1", "50% 50u1"],
-    ["center left 50%", "50% 50%"],
-    ["center left 50u1", "50u1 50%"],
-    ["center right 70%", "30% 50%"],
-    ["center right 70u1", "right 70u1 top 50%"],
-    ["center bottom 70%", "50% 30%"],
-    ["center bottom 70u1", "left 50% bottom 70u1"],
-
-    ["left top 50%", "0% 50%"],
-    ["left top 50u1", "0% 50u1"],
-    ["left bottom 70%", "0% 30%"],
-    ["left bottom 70u1", "left 0% bottom 70u1"],
-
-    ["top left 50%", "50% 0%"],
-    ["top left 50u1", "50u1 0%"],
-    ["top right 70%", "30% 0%"],
-    ["top right 70u1", "right 70u1 top 0%"],
-
-    ["bottom left 50%", "50% 100%"],
-    ["bottom left 50u1", "50u1 100%"],
-    ["bottom right 70%", "30% 100%"],
-    ["bottom right 70u1", "right 70u1 top 100%"],
-
-    ["right bottom 70%", "100% 30%"],
-    ["right bottom 70u1", "left 100% bottom 70u1"],
-    ["right top 50%", "100% 50%"],
-    ["right top 50u1", "100% 50u1"],
-
-////// [ keyword percent | keyword], [ keyword length | keyword ] x 5 keywords
-    ["left 50% center", "50% 50%"],
-    ["left 50u1 center", "50u1 50%"],
-    ["left 50% top", "50% 0%"],
-    ["left 50u1 top", "50u1 0%"],
-    ["left 50% bottom", "50% 100%"],
-    ["left 50u1 bottom", "50u1 100%"],
-
-    ["top 50% center", "50% 50%"],
-    ["top 50u1 center", "50% 50u1"],
-    ["top 50% left", "0% 50%"],
-    ["top 50u1 left", "0% 50u1"],
-    ["top 50% right", "100% 50%"],
-    ["top 50u1 right", "100% 50u1"],
-
-    ["bottom 70% center", "50% 30%"],
-    ["bottom 70u1 center", "left 50% bottom 70u1"],
-    ["bottom 70% left", "0% 30%"],
-    ["bottom 70u1 left", "left 0% bottom 70u1"],
-    ["bottom 70% right", "100% 30%"],
-    ["bottom 70u1 right", "left 100% bottom 70u1"],
-
-    ["right 80% center", "20% 50%"],
-    ["right 80u1 center", "right 80u1 top 50%"],
-    ["right 80% bottom", "20% 100%"],
-    ["right 80u1 bottom", "right 80u1 top 100%"],
-    ["right 80% top", "20% 0%"],
-    ["right 80u1 top", "right 80u1 top 0%"],
-
 ////// [ keyword percent |  keyword percent], [ keyword percent |  keyword length],
 ////// [ keyword length | keyword length],  [ keyword length | keyword percent] x 5 keywords
     ["left 50% top 50%", "50% 50%"],
diff --git a/css/css-sizing/OWNERS b/css/css-sizing/OWNERS
new file mode 100644
index 0000000..ea4a9a0
--- /dev/null
+++ b/css/css-sizing/OWNERS
@@ -0,0 +1,2 @@
+@tabatkins
+@fantasai
diff --git a/css/css-syntax/declarations-trim-whitespace.html b/css/css-syntax/declarations-trim-whitespace.html
new file mode 100644
index 0000000..a7d69d1
--- /dev/null
+++ b/css/css-syntax/declarations-trim-whitespace.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Declarations trim whitespace from their beginning/ending</title>
+<meta name="author" title="Tab Atkins-Bittner">
+<link rel=help href="https://drafts.csswg.org/css-syntax/#consume-declaration">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+    <div id="log"></div>
+    <style id="style-el">
+    #foo {
+        --foo-1:bar;
+        --foo-2: bar;
+        --foo-3:bar ;
+        --foo-4: bar ;
+        --foo-5: bar !important;
+        --foo-6: bar !important ;
+        --foo-7:bar!important;
+        --foo-8:bar!important ;
+        --foo-9:bar
+    }
+    </style>
+    <p id=foo>foo</p>
+    <script>
+    let stylesheet = getComputedStyle(document.querySelector("#foo"));
+    let canonical = "bar";
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-1"), canonical);
+    }, "--foo-1:bar;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-2"), canonical);
+    }, "--foo-2: bar;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-3"), canonical);
+    }, "--foo-3:bar ;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-4"), canonical);
+    }, "--foo-4: bar ;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-5"), canonical);
+    }, "--foo-5: bar !important;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-6"), canonical);
+    }, "--foo-6: bar !important ;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-7"), canonical);
+    }, "--foo-7:bar!important;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-8"), canonical);
+    }, "--foo-8:bar!important ;");
+    test(function() {
+        assert_equals(stylesheet.getPropertyValue("--foo-9"), canonical);
+    }, "--foo-9:bar (then ws until end of rule)");
+    </script>
diff --git a/css/css-tables/anonymous-table-ws-001-ref.html b/css/css-tables/anonymous-table-ws-001-ref.html
new file mode 100644
index 0000000..4429239
--- /dev/null
+++ b/css/css-tables/anonymous-table-ws-001-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<title>CSS Reference</title>
+<style>
+div { font: 16px monospace; }
+</style>
+<p>Test passes if there is a space between the "a" and "b".</p>
+<div>a b</div>
diff --git a/css/css-tables/anonymous-table-ws-001.html b/css/css-tables/anonymous-table-ws-001.html
new file mode 100644
index 0000000..99d2cfc
--- /dev/null
+++ b/css/css-tables/anonymous-table-ws-001.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>CSS Test: White space should not be removed between elements for which a single anonymous table cell is generated</title>
+<link rel="author" title="Cameron McCormack" href="mailto:cam@mcc.id.au">
+<link rel="match" href="anonymous-table-ws-001-ref.html">
+<link rel="help" href="https://drafts.csswg.org/css-tables/#fixup-algorithm">
+<style>
+div { display: table; font: 16px monospace; }
+</style>
+<p>Test passes if there is a space between the "a" and "b".</p>
+<div>
+  <span>a</span> <span>b</span>
+</div>
diff --git a/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002-ref.html b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002-ref.html
new file mode 100644
index 0000000..37fe964
--- /dev/null
+++ b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002-ref.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reftest Reference: Percentage sizing of table cell children with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.table {
+  display: block;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+}
+
+.td {
+  background: cyan;
+  overflow: scroll;
+  padding: 5px 15px 10px 20px;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  height: 100px;
+  box-sizing: border-box;
+}
+
+.child {
+  background: yellow;
+  width: 100%;
+  height: 100%;
+}
+</style>
+
+<p>The test passes if you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="table">
+  <div class="td">
+    <div class="child"></div>
+  </div>
+</div>
diff --git a/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002.html b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002.html
new file mode 100644
index 0000000..814aa27
--- /dev/null
+++ b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-children-002.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Table Test: Percentage sizing of table cell children with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-tables-3/#content-measure">
+<link rel="match" href="percentage-sizing-of-table-cell-children-002-ref.html">
+<meta name="assert" content="Checks that table cell children resolve properly their percentage sizes, even when the table cell has margin, border, padding and scrollbar.">
+<style>
+.table {
+  display: table;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+}
+
+.td {
+  display: table-cell;
+  background: cyan;
+  overflow: scroll;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+}
+
+.child {
+  background: yellow;
+  width: 100%;
+  height: 100%;
+}
+</style>
+
+<p>The test passes if you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="table">
+  <div class="td">
+    <div class="child"></div>
+  </div>
+</div>
diff --git a/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001-ref.html b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001-ref.html
new file mode 100644
index 0000000..3919e1e
--- /dev/null
+++ b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001-ref.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reftest Reference: Percentage sizing of table cell children with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.table {
+  display: block;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+}
+
+.td {
+  background: cyan;
+  overflow: scroll;
+  padding: 5px 15px 10px 20px;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+  height: 100px;
+  box-sizing: border-box;
+}
+
+img {
+  display: block;
+  background: yellow;
+  width: 100%;
+  height: 100%;
+}
+</style>
+
+<p>The test passes if you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="table">
+  <div class="td">
+    <img />
+  </div>
+</div>
diff --git a/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001.html b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001.html
new file mode 100644
index 0000000..2d36738
--- /dev/null
+++ b/css/css-tables/height-distribution/percentage-sizing-of-table-cell-replaced-children-001.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Table Test: Percentage sizing of table cell replaced children with margin, border, padding and scrollbar</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-tables-3/#content-measure">
+<link rel="match" href="percentage-sizing-of-table-cell-replaced-children-001-ref.html">
+<meta name="assert" content="Checks that table cell replaced children resolve properly their percentage sizes, even when the table cell has margin, border, padding and scrollbar.">
+<style>
+.table {
+  display: table;
+  border: solid 5px black;
+  width: 150px;
+  height: 100px;
+}
+
+.td {
+  display: table-cell;
+  background: cyan;
+  overflow: scroll;
+  margin: 1px 2px 3px 4px;
+  padding: 5px 15px 10px 20px;
+  border: solid magenta;
+  border-width: 12px 9px 6px 3px;
+}
+
+img {
+  display: block;
+  background: yellow;
+  width: 100%;
+  height: 100%;
+}
+</style>
+
+<p>The test passes if you see scrollbars but there's no overflow, so you cannot actually scroll.</p>
+
+<div class="table">
+  <div class="td">
+    <img />
+  </div>
+</div>
diff --git a/css/css-text/overflow-wrap/overflow-wrap-break-word-002.html b/css/css-text/overflow-wrap/overflow-wrap-break-word-002.html
new file mode 100644
index 0000000..f4e9493
--- /dev/null
+++ b/css/css-text/overflow-wrap/overflow-wrap-break-word-002.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Text Test: overflow-wrap: break-word+break-spaces</title>
+<link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net/">
+<link rel="help" href="https://drafts.csswg.org/css-text-3/#valdef-overflow-wrap-break-word">
+<link rel="help" href="https://drafts.csswg.org/css-text-3/#valdef-overflow-wrap-break-spaces">
+<meta name="flags" content="ahem">
+<link rel="match" href="reference/overflow-wrap-break-word-002-ref.html">
+<meta name="assert" content="break-word + break-spaces do not allow a break
+between the last character of a word and the first space of a sequence of preserved spaces
+if there are other wrapping opportunities earlier in the line">
+<style>
+div {
+  white-space: pre-wrap;
+  overflow-wrap: break-word break-spaces;
+  font-family: monospace;
+  width: 5ch;
+  line-height: 1;
+  overflow: hidden;
+  height: 1em;
+}
+</style>
+
+<p>This test passes if there is nothing below this sentence.
+<div> FAIL <div>
+<!--
+white-space:pre-wrap + overflow:break-spaces should cause the spaces at the end of the line to be preserved.
+Since there is an allowed break point between the first space and the F,
+that's where the line should wrap,
+not between the L and the subsequent space.
+-->
diff --git a/css/css-text/overflow-wrap/overflow-wrap-break-word-003.html b/css/css-text/overflow-wrap/overflow-wrap-break-word-003.html
new file mode 100644
index 0000000..21e0af3
--- /dev/null
+++ b/css/css-text/overflow-wrap/overflow-wrap-break-word-003.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Text Test: overflow-wrap: break-word+break-spaces</title>
+<link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net/">
+<link rel="help" href="https://drafts.csswg.org/css-text-3/#valdef-overflow-wrap-break-word">
+<link rel="help" href="https://drafts.csswg.org/css-text-3/#valdef-overflow-wrap-break-spaces">
+<meta name="flags" content="ahem">
+<link rel="match" href="reference/overflow-wrap-break-word-003-ref.html">
+<meta name="assert" content="break-word + break-spaces do allow a break
+between the last character of a word and the first space of a sequence of preserved spaces
+if there are no other wrapping opportunities earlier in the line">
+<style>
+div {
+  white-space: pre-wrap;
+  overflow-wrap: break-word break-spaces;
+  font-family: monospace;
+  width: 4ch;
+  line-height: 1;
+  overflow: hidden;
+  height: 2em;
+}
+</style>
+
+<p>This test passes if the word FAIL does not appear below.
+<div>PASS FAIL<div>
diff --git a/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-002-ref.html b/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-002-ref.html
new file mode 100644
index 0000000..5dca683
--- /dev/null
+++ b/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-002-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>test reference</title>
+<link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net/">
+
+<p>This test passes if there is nothing below this sentence.
diff --git a/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-003-ref.html b/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-003-ref.html
new file mode 100644
index 0000000..6f7315d
--- /dev/null
+++ b/css/css-text/overflow-wrap/reference/overflow-wrap-break-word-003-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Text Test: overflow-wrap: break-word+break-spaces</title>
+<link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net/">
+<style>
+div {
+  font-family: monospace;
+  line-height: 1;
+}
+</style>
+
+<p>This test passes if the word FAIL does not appear below.
+<div>PASS<div>
diff --git a/css/css-transforms/css-transform-animate-translate-implied-y-ref.html b/css/css-transforms/css-transform-animate-translate-implied-y-ref.html
new file mode 100644
index 0000000..43d1737
--- /dev/null
+++ b/css/css-transforms/css-transform-animate-translate-implied-y-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>CSS Reftest Reference</title>
+    <link rel="author" title="Manish Goregaokar" href="mailto:manishearth@gmail.com">
+    <style>
+        #box {
+            background:red;
+            width: 200px;
+            height: 200px;
+            transform: translate(100px, 0px);
+        }
+    </style>
+  </head>
+  <body>
+    <div id=box></div>
+  </body>
+</html>
diff --git a/css/css-transforms/css-transform-animate-translate-implied-y.html b/css/css-transforms/css-transform-animate-translate-implied-y.html
new file mode 100644
index 0000000..dc0b957
--- /dev/null
+++ b/css/css-transforms/css-transform-animate-translate-implied-y.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>CSS Test (Transforms): Animating between translates where one has an implied `y` value</title>
+    <link rel="author" title="Manish Goregaokar" href="mailto:manishearth@gmail.com">
+    <meta name="assert" content='This tests that translate(x) is animated as if it were translate(x, 0px)'>
+    <link rel=match href=css-transform-animate-translate-implied-y-ref.html>
+    <link rel="help" href="https://drafts.csswg.org/css-transforms-1/#funcdef-transform-translate">
+    <style>
+    #box {
+        background:red;
+        width: 200px;
+        height: 200px;
+    }
+    </style>
+  </head>
+  <body>
+    <div id=box></div>
+    <script>
+        let anim = document.getElementById('box').animate([
+            { transform: 'translate(180px)' },
+            { transform: 'translate(20px, 0px)' }
+        ], {
+            duration: 1000,
+        });
+        anim.pause();
+        anim.currentTime = 500;
+    </script>
+  </body>
+</html>
diff --git a/css/css-transforms/parsing/perspective-origin-parsing-invalid.html b/css/css-transforms/parsing/perspective-origin-parsing-invalid.html
index e9bb174..09f2f47 100644
--- a/css/css-transforms/parsing/perspective-origin-parsing-invalid.html
+++ b/css/css-transforms/parsing/perspective-origin-parsing-invalid.html
@@ -19,7 +19,6 @@
 
 // The following were supported in an earlier version of the spec.
 // https://github.com/w3c/csswg-drafts/issues/2140
-// Deprecated in Blink with support to be removed in M68, around July 2018.
 test_invalid_value("perspective-origin", "center left 1px");
 test_invalid_value("perspective-origin", "center top 2px");
 test_invalid_value("perspective-origin", "right 3% center");
diff --git a/css/css-transitions/transition-001.html b/css/css-transitions/transition-001.html
index bf01393..e0bf09a 100644
--- a/css/css-transitions/transition-001.html
+++ b/css/css-transitions/transition-001.html
@@ -24,26 +24,24 @@
 
         <script>
             var transition = document.getElementById('transition');
-            var ease = 'cubic-bezier(0.25, 0.1, 0.25, 1)';
-            var easeIn = 'cubic-bezier(0.42, 0, 1, 1)';
             // Note that order is important in this property. The first value that can be parsed as a time is assigned to
             // the transition-duration. The second value that can be parsed as a time is assigned to transition-delay.
             // [<‘transition-property’> || <‘transition-duration’> || <‘transition-timing-function’> || <‘transition-delay’> [, [<‘transition-property’> || <‘transition-duration’> || <‘transition-timing-function’> || <‘transition-delay’>]]*
             var values = {
                 // [property, duration, timing, delay]
                 // random order
-                '1s' : ["all", "1s", ease, "0s"],
-                '1s 2s' : ["all", "1s", ease, "2s"],
-                '1s 2s ease-in' : ["all", "1s", easeIn, "2s"],
-                '1s ease-in 2s' : ["all", "1s", easeIn, "2s"],
-                'ease-in 1s 2s' : ["all", "1s", easeIn, "2s"],
-                '1s width' : ["width", "1s", ease, "0s"],
-                'width 1s' : ["width", "1s", ease, "0s"],
-                '1s width 2s' : ["width", "1s", ease, "2s"],
-                '1s 2s width ease-in' : ["width", "1s", easeIn, "2s"],
-                '1s ease-in 2s width' : ["width", "1s", easeIn, "2s"],
-                'width ease-in 1s 2s' : ["width", "1s", easeIn, "2s"],
-                'width .1s ease-in .2s' : ["width", "0.1s", easeIn, "0.2s"]
+                '1s' : ["all", "1s", "ease", "0s"],
+                '1s 2s' : ["all", "1s", "ease", "2s"],
+                '1s 2s ease-in' : ["all", "1s", "ease-in", "2s"],
+                '1s ease-in 2s' : ["all", "1s", "ease-in", "2s"],
+                'ease-in 1s 2s' : ["all", "1s", "ease-in", "2s"],
+                '1s width' : ["width", "1s", "ease", "0s"],
+                'width 1s' : ["width", "1s", "ease", "0s"],
+                '1s width 2s' : ["width", "1s", "ease", "2s"],
+                '1s 2s width ease-in' : ["width", "1s", "ease-in", "2s"],
+                '1s ease-in 2s width' : ["width", "1s", "ease-in", "2s"],
+                'width ease-in 1s 2s' : ["width", "1s", "ease-in", "2s"],
+                'width .1s ease-in .2s' : ["width", "0.1s", "ease-in", "0.2s"]
             };
 
             for (var key in values) {
diff --git a/css/css-transitions/transition-timing-function-001.html b/css/css-transitions/transition-timing-function-001.html
index 68669c7..7566545 100644
--- a/css/css-transitions/transition-timing-function-001.html
+++ b/css/css-transitions/transition-timing-function-001.html
@@ -24,15 +24,14 @@
 
         <script>
             var transition = document.getElementById('transition');
-            // "ease"
-            var defaultValue = 'cubic-bezier(0.25, 0.1, 0.25, 1)';
+            var defaultValue = 'ease';
             var values = {
                 // keywords
-                'ease': 'cubic-bezier(0.25, 0.1, 0.25, 1)',
-                'linear': 'cubic-bezier(0, 0, 1, 1)',
-                'ease-in': 'cubic-bezier(0.42, 0, 1, 1)',
-                'ease-out': 'cubic-bezier(0, 0, 0.58, 1)',
-                'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1)',
+                'ease': 'ease',
+                'linear': 'linear',
+                'ease-in': 'ease-in',
+                'ease-out': 'ease-out',
+                'ease-in-out': 'ease-in-out',
                 'step-start': 'steps(1, start)',
                 'step-end': 'steps(1)',
                 // cubic bezier
diff --git a/css/css-typed-om/stylevalue-normalization/positionvalue-normalization.tentative.html b/css/css-typed-om/stylevalue-normalization/positionvalue-normalization.tentative.html
index 15f6742..8631c05 100644
--- a/css/css-typed-om/stylevalue-normalization/positionvalue-normalization.tentative.html
+++ b/css/css-typed-om/stylevalue-normalization/positionvalue-normalization.tentative.html
@@ -52,9 +52,8 @@
   for (const {cssText: yCssText, y, type: yType} of gYTestCases) {
     const cssText = xCssText + ' ' + yCssText;
 
-    // Can't have things like left 10px 20px
-    if ((xType == 'offset' && yType == 'length') ||
-        (xType == 'length' && yType == 'offset'))
+    // Can't have position values with 3 parts
+    if ((xType === 'offset') !== (yType === 'offset'))
       continue;
 
     test(t => {
diff --git a/css/css-typed-om/the-stylepropertymap/properties/all.html b/css/css-typed-om/the-stylepropertymap/properties/all.html
new file mode 100644
index 0000000..dd618bc
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/all.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'all' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('all', []);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/animation-name.html b/css/css-typed-om/the-stylepropertymap/properties/animation-name.html
new file mode 100644
index 0000000..df5d44a
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/animation-name.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'animation-name' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runListValuedPropertyTests('animation-name', [
+  { syntax: 'none' },
+  // FIXME: This should be <custom-ident>, but the test harness doesn't
+  // currently support it.
+  { syntax: 'custom-ident' },
+]);
+
+runUnsupportedPropertyTests('animation-name', [
+  '"foo"'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/backdrop-filter.html b/css/css-typed-om/the-stylepropertymap/properties/backdrop-filter.html
new file mode 100644
index 0000000..837ceb7
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/backdrop-filter.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'backdrop-filter' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('backdrop-filter', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('filter', [
+  'blur(2px)',
+  'url(filters.svg) blur(4px) saturate(150%)',
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/box-shadow.html b/css/css-typed-om/the-stylepropertymap/properties/box-shadow.html
new file mode 100644
index 0000000..040e992
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/box-shadow.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'box-shadow' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('box-shadow', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('box-shadow', [
+  '10px 5px 5px red',
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/contain.html b/css/css-typed-om/the-stylepropertymap/properties/contain.html
new file mode 100644
index 0000000..0202da4
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/contain.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'contain' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('contain', [
+  { syntax: 'none' },
+  { syntax: 'strict' },
+  { syntax: 'content' },
+  { syntax: 'size' },
+  { syntax: 'layout' },
+  { syntax: 'style' },
+  { syntax: 'paint' },
+]);
+
+runUnsupportedPropertyTests('contain', [
+  'size layout', 'paint style layout size'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/counter-increment.html b/css/css-typed-om/the-stylepropertymap/properties/counter-increment.html
new file mode 100644
index 0000000..0dba637
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/counter-increment.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'counter-increment' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('counter-increment', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('counter-increment', [
+  'chapter', 'chapter 3'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/counter-reset.html b/css/css-typed-om/the-stylepropertymap/properties/counter-reset.html
new file mode 100644
index 0000000..5323f18
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/counter-reset.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'counter-reset' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('counter-reset', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('counter-reset', [
+  'chapter', 'chapter 3'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/cursor.html b/css/css-typed-om/the-stylepropertymap/properties/cursor.html
new file mode 100644
index 0000000..7465b1a
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/cursor.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'cursor' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('cursor', [
+  { syntax: 'auto' },
+  { syntax: 'default' },
+  { syntax: 'none' },
+  { syntax: 'context-menu' },
+  { syntax: 'help' },
+  { syntax: 'pointer' },
+  { syntax: 'progress' },
+  { syntax: 'wait' },
+  { syntax: 'cell' },
+  { syntax: 'crosshair' },
+  { syntax: 'text' },
+  { syntax: 'vertical-text' },
+  { syntax: 'alias' },
+  { syntax: 'copy' },
+  { syntax: 'move' },
+  { syntax: 'no-drop' },
+  { syntax: 'not-allowed' },
+  { syntax: 'grab' },
+  { syntax: 'grabbing' },
+  { syntax: 'e-resize' },
+  { syntax: 'n-resize' },
+  { syntax: 'ne-resize' },
+  { syntax: 'nw-resize' },
+  { syntax: 's-resize' },
+  { syntax: 'se-resize' },
+  { syntax: 'sw-resize' },
+  { syntax: 'w-resize' },
+  { syntax: 'ew-resize' },
+  { syntax: 'ns-resize' },
+  { syntax: 'nesw-resize' },
+  { syntax: 'nwse-resize' },
+  { syntax: 'col-resize' },
+  { syntax: 'row-resize' },
+  { syntax: 'all-scroll' },
+  { syntax: 'zoom-in' },
+  { syntax: 'zoom-out' }
+]);
+
+runUnsupportedPropertyTests('cursor', [
+  'url(hand.cur), pointer', 'url(cursor1.png) 4 12, auto'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/filter.html b/css/css-typed-om/the-stylepropertymap/properties/filter.html
new file mode 100644
index 0000000..62ddeff
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/filter.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'filter' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('filter', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('filter', [
+  'blur(2px)',
+  'url(filters.svg) blur(4px) saturate(150%)',
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/lighting-color.html b/css/css-typed-om/the-stylepropertymap/properties/lighting-color.html
new file mode 100644
index 0000000..aec1643
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/lighting-color.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'lighting-color' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('lighting-color', [
+  {
+    syntax: 'currentcolor',
+    // computes to a <color>, which is not supported in level 1
+    computed: (_, result) => assert_class_string(result, 'CSSStyleValue')
+  }
+]);
+
+// <color>s are not supported in level 1
+runUnsupportedPropertyTests('lighting-color', [
+  'red', '#bbff00', 'rgb(255, 255, 128)', 'hsl(50, 33%, 25%)',
+  'transparent'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/line-height-step.html b/css/css-typed-om/the-stylepropertymap/properties/line-height-step.html
new file mode 100644
index 0000000..2a25562
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/line-height-step.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'line-height-step' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('line-height-step', [
+  {
+    syntax: '<length>',
+    specified: assert_is_equal_with_range_handling
+  }
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/list-style-type.html b/css/css-typed-om/the-stylepropertymap/properties/list-style-type.html
new file mode 100644
index 0000000..61e7540
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/list-style-type.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'list-style-type' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('list-style-type', [
+  { syntax: 'none' },
+  // FIXME: This should be <custom-ident>, but the test harness doesn't
+  // currently support it.
+  { syntax: 'custom-ident' },
+]);
+
+runUnsupportedPropertyTests('list-style-type', [
+  '"Note: "', 'symbols("*" "A" "B" "C")'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/offset-rotate.html b/css/css-typed-om/the-stylepropertymap/properties/offset-rotate.html
index 56a8580..c91e27c 100644
--- a/css/css-typed-om/the-stylepropertymap/properties/offset-rotate.html
+++ b/css/css-typed-om/the-stylepropertymap/properties/offset-rotate.html
@@ -12,6 +12,12 @@
 <script>
 'use strict';
 
+runPropertyTests('offset-rotate', [
+  { syntax: 'auto' },
+  { syntax: 'reverse' },
+  { syntax: '<angle>' },
+]);
+
 runUnsupportedPropertyTests('offset-rotate', [
   'auto 90deg',
   'reverse -90deg',
diff --git a/css/css-typed-om/the-stylepropertymap/properties/order.html b/css/css-typed-om/the-stylepropertymap/properties/order.html
new file mode 100644
index 0000000..35b9d26
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/order.html
@@ -0,0 +1,35 @@
+<meta charset="utf-8">
+<title>'order' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('order', [
+  {
+    syntax: '<number>',
+    // order needs to be an integer
+    specified: (input, result) => {
+      if (input instanceof CSSUnitValue && !Number.isInteger(input.value))
+        assert_style_value_equals(result, new CSSMathSum(input));
+      else
+        assert_style_value_equals(result, input);
+    },
+    computed: (input, result) => {
+      const number = input.to('number');
+      if (!Number.isInteger(number.value))
+        assert_style_value_equals(result, new CSSUnitValue(Math.round(number.value), 'number'));
+      else
+        assert_style_value_equals(result, number);
+    }
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/orphans.html b/css/css-typed-om/the-stylepropertymap/properties/orphans.html
new file mode 100644
index 0000000..7d75584
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/orphans.html
@@ -0,0 +1,37 @@
+<meta charset="utf-8">
+<title>'orphans' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('orphans', [
+  {
+    syntax: '<number>',
+    // orphans needs to be a positive integer
+    specified: (input, result) => {
+      if (input instanceof CSSUnitValue && (!Number.isInteger(input.value) || input.value < 1))
+        assert_style_value_equals(result, new CSSMathSum(input));
+      else
+        assert_style_value_equals(result, input);
+    },
+    computed: (input, result) => {
+      const number = input.to('number');
+      if (number < 1)
+        assert_style_value_equals(result, new CSSUnitValue(1, 'number'));
+      else if (!Number.isInteger(number.value))
+        assert_style_value_equals(result, new CSSUnitValue(Math.round(number.value), 'number'));
+      else
+        assert_style_value_equals(result, number);
+    }
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/outline-offset.html b/css/css-typed-om/the-stylepropertymap/properties/outline-offset.html
new file mode 100644
index 0000000..f9c0f56
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/outline-offset.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'outline-offset' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('outline-offset', [
+  { syntax: '<length>' },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/outline-width.html b/css/css-typed-om/the-stylepropertymap/properties/outline-width.html
new file mode 100644
index 0000000..3686741
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/outline-width.html
@@ -0,0 +1,41 @@
+<meta charset="utf-8">
+<title>'outline-width' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_is_zero_px(result) {
+  assert_style_value_equals(result, new CSSUnitValue(0, 'px'));
+}
+
+runPropertyTests('outline-width', [
+  // Computed value is 0 when outline-style is 'none'.
+  // FIXME: Add separate test where outline-style is not 'none' or 'hidden'.
+  {
+    syntax: 'thin',
+    computed: (_, result) => assert_is_zero_px(result)
+  },
+  {
+    syntax: 'medium',
+    computed: (_, result) => assert_is_zero_px(result)
+  },
+  {
+    syntax: 'thick',
+    computed: (_, result) => assert_is_zero_px(result)
+  },
+  {
+    syntax: '<length>',
+    specified: assert_is_equal_with_range_handling,
+    computed: (_, result) => assert_is_zero_px(result)
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/overscroll-behavior.html b/css/css-typed-om/the-stylepropertymap/properties/overscroll-behavior.html
new file mode 100644
index 0000000..5e440b2
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/overscroll-behavior.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'overscroll-behavior' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+for (const suffix of ['x', 'y']) {
+  runPropertyTests(`overscroll-behavior-${suffix}`, [
+    { syntax: 'contain' },
+    { syntax: 'none' },
+    { syntax: 'auto' }
+  ]);
+}
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/page.html b/css/css-typed-om/the-stylepropertymap/properties/page.html
new file mode 100644
index 0000000..93255b8
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/page.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'page' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('page', [
+  { syntax: 'auto' },
+  // FIXME: This should be <custom-ident>, but the test harness doesn't
+  // currently support it.
+  { syntax: 'custom-ident' },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/perspective-origin.html b/css/css-typed-om/the-stylepropertymap/properties/perspective-origin.html
new file mode 100644
index 0000000..a72f79c
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/perspective-origin.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'perspective-origin' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('perspective-origin', [
+  { syntax: 'none' },
+  { syntax: '<position>' },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/perspective.html b/css/css-typed-om/the-stylepropertymap/properties/perspective.html
new file mode 100644
index 0000000..d90d939
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/perspective.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'perspective' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('perspective', [
+  { syntax: 'none' },
+  {
+    syntax: '<length>',
+    specified: assert_is_equal_with_range_handling
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/quotes.html b/css/css-typed-om/the-stylepropertymap/properties/quotes.html
new file mode 100644
index 0000000..99e8442
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/quotes.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'quotes' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('quotes', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('quotes', [
+  '"<<" ">>" "<" ">"'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js b/css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js
index db79630..3ca0b55 100644
--- a/css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js
+++ b/css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js
@@ -124,6 +124,60 @@
       }
     ],
   },
+  '<time>': {
+    description: 'a time',
+    examples: [
+      {
+        description: "zero seconds",
+        input: new CSSUnitValue(0, 's')
+      },
+      {
+        description: "negative milliseconds",
+        input: new CSSUnitValue(-3.14, 'ms'),
+        // Computed values use canonical units
+        defaultComputed: (_, result) => assert_style_value_equals(result, new CSSUnitValue(-0.00314, 's'))
+      },
+      {
+        description: "positive seconds",
+        input: new CSSUnitValue(3.14, 's')
+      },
+      {
+        description: "a calc time",
+        input: new CSSMathSum(new CSSUnitValue(0, 's'), new CSSUnitValue(0, 'ms')),
+        // Specified/computed calcs are usually simplified.
+        // FIXME: Test this properly
+        defaultSpecified: (_, result) => assert_is_calc_sum(result),
+        defaultComputed: (_, result) => assert_is_unit('s', result)
+      }
+    ],
+  },
+  '<angle>': {
+    description: 'an angle',
+    examples: [
+      {
+        description: "zero degrees",
+        input: new CSSUnitValue(0, 'deg')
+      },
+      {
+        description: "positive radians",
+        input: new CSSUnitValue(3.14, 'rad'),
+        // Computed values use canonical units
+        defaultComputed: (_, result) => assert_style_value_equals(result, new CSSUnitValue(179.908752, 'deg'))
+      },
+      {
+        description: "negative degrees",
+        input: new CSSUnitValue(-3.14, 'deg')
+      },
+      {
+        description: "a calc angle",
+        input: new CSSMathSum(new CSSUnitValue(0, 'rad'), new CSSUnitValue(0, 'deg')),
+        // Specified/computed calcs are usually simplified.
+        // FIXME: Test this properly
+        defaultSpecified: (_, result) => assert_is_calc_sum(result),
+        defaultComputed: (_, result) => assert_is_unit('deg', result)
+      }
+    ],
+  },
   '<flex>': {
     description: 'a flexible length',
     examples: [
diff --git a/css/css-typed-om/the-stylepropertymap/properties/shape-image-threshold.html b/css/css-typed-om/the-stylepropertymap/properties/shape-image-threshold.html
new file mode 100644
index 0000000..d16d892
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/shape-image-threshold.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'shape-image-threshold' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_is_equal_with_clamping(input, result) {
+  const number = input.to('number');
+
+  if (number.value < 0)
+    assert_style_value_equals(result, new CSSUnitValue(0, 'number'));
+  else if (number.value > 1)
+    assert_style_value_equals(result, new CSSUnitValue(1, 'number'));
+  else
+    assert_style_value_equals(result, input);
+}
+
+runPropertyTests('shape-image-threshold', [
+  {
+    syntax: '<number>',
+    computed: assert_is_equal_with_clamping
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/shape-margin.html b/css/css-typed-om/the-stylepropertymap/properties/shape-margin.html
new file mode 100644
index 0000000..ca517bb
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/shape-margin.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'shape-margin' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('shape-margin', [
+  {
+    syntax: '<length>',
+    specified: assert_is_equal_with_range_handling
+  },
+  {
+    syntax: '<percentage>',
+    specified: assert_is_equal_with_range_handling
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/speak.html b/css/css-typed-om/the-stylepropertymap/properties/speak.html
new file mode 100644
index 0000000..33512fe
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/speak.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'speak' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('speak', [
+  { syntax: 'auto' },
+  { syntax: 'never' },
+  { syntax: 'always' },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/tab-size.html b/css/css-typed-om/the-stylepropertymap/properties/tab-size.html
new file mode 100644
index 0000000..d03139e
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/tab-size.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'tab-size' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('tab-size', [
+  {
+    // tab-size can be a non-negative integer
+    syntax: '<number>',
+    specified: (input, result) => {
+      if (input instanceof CSSUnitValue && (!Number.isInteger(input.value) || input.value < 0))
+        assert_style_value_equals(result, new CSSMathSum(input));
+      else
+        assert_style_value_equals(result, input);
+    },
+    computed: (input, result) => {
+      const number = input.to('number');
+      if (number < 0)
+        assert_style_value_equals(result, new CSSUnitValue(0, 'number'));
+      else if (!Number.isInteger(number.value))
+        assert_style_value_equals(result, new CSSUnitValue(Math.round(number.value), 'number'));
+      else
+        assert_style_value_equals(result, number);
+    }
+  },
+  {
+    syntax: '<length>',
+    specified: assert_is_equal_with_range_handling
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/touch-action.html b/css/css-typed-om/the-stylepropertymap/properties/touch-action.html
new file mode 100644
index 0000000..2435e34
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/touch-action.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'touch-action' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('touch-action', [
+  { syntax: 'auto' },
+  { syntax: 'none' },
+  { syntax: 'pan-x' },
+  { syntax: 'pan-left' },
+  { syntax: 'pan-right' },
+  { syntax: 'pan-y' },
+  { syntax: 'pan-up' },
+  { syntax: 'pan-down' },
+  { syntax: 'pinch-zoom' },
+  { syntax: 'manipulation' },
+]);
+
+runUnsupportedPropertyTests('touch-action', [
+  'pan-x pan-down', 'pan-down pinch-zoom pan-right'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/transform-box.html b/css/css-typed-om/the-stylepropertymap/properties/transform-box.html
new file mode 100644
index 0000000..a5556b1
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/transform-box.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'transform-box' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('transform-box', [
+  { syntax: 'border-box' },
+  { syntax: 'fill-box' },
+  { syntax: 'view-box' },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/transition-property.html b/css/css-typed-om/the-stylepropertymap/properties/transition-property.html
new file mode 100644
index 0000000..7f0c48a
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/transition-property.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'transition-property' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('transition-property', [
+  { syntax: 'none' },
+]);
+
+runUnsupportedPropertyTests('transition-property', [
+  'width', 'width, height', 'all'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/widows.html b/css/css-typed-om/the-stylepropertymap/properties/widows.html
new file mode 100644
index 0000000..7503bff
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/widows.html
@@ -0,0 +1,37 @@
+<meta charset="utf-8">
+<title>'widows' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('widows', [
+  {
+    syntax: '<number>',
+    // widows needs to be a positive integer
+    specified: (input, result) => {
+      if (input instanceof CSSUnitValue && (!Number.isInteger(input.value) || input.value < 1))
+        assert_style_value_equals(result, new CSSMathSum(input));
+      else
+        assert_style_value_equals(result, input);
+    },
+    computed: (input, result) => {
+      const number = input.to('number');
+      if (number < 1)
+        assert_style_value_equals(result, new CSSUnitValue(1, 'number'));
+      else if (!Number.isInteger(number.value))
+        assert_style_value_equals(result, new CSSUnitValue(Math.round(number.value), 'number'));
+      else
+        assert_style_value_equals(result, number);
+    }
+  },
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/will-change.html b/css/css-typed-om/the-stylepropertymap/properties/will-change.html
new file mode 100644
index 0000000..2add50c
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/will-change.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'will-change' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('will-change', [
+  { syntax: 'auto' },
+]);
+
+runUnsupportedPropertyTests('will-change', [
+  'scroll-position', 'contents, foo, scroll-position'
+]);
+
+</script>
diff --git a/css/css-typed-om/the-stylepropertymap/properties/z-index.html b/css/css-typed-om/the-stylepropertymap/properties/z-index.html
new file mode 100644
index 0000000..53773fb
--- /dev/null
+++ b/css/css-typed-om/the-stylepropertymap/properties/z-index.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>'z-index' property</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/testhelper.js"></script>
+<script src="resources/testsuite.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+runPropertyTests('z-index', [
+  { syntax: 'auto' },
+  {
+    syntax: '<number>',
+    // z-index needs to be an integer
+    specified: (input, result) => {
+      if (input instanceof CSSUnitValue && !Number.isInteger(input.value))
+        assert_style_value_equals(result, new CSSMathSum(input));
+      else
+        assert_style_value_equals(result, input);
+    },
+    computed: (input, result) => {
+      const number = input.to('number');
+      if (!Number.isInteger(number.value))
+        assert_style_value_equals(result, new CSSUnitValue(Math.round(number.value), 'number'));
+      else
+        assert_style_value_equals(result, number);
+    }
+  }
+]);
+
+</script>
diff --git a/css/css-ui/parsing/box-sizing-invalid.html b/css/css-ui/parsing/box-sizing-invalid.html
new file mode 100644
index 0000000..90bb5a1
--- /dev/null
+++ b/css/css-ui/parsing/box-sizing-invalid.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing box-sizing with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#box-sizing">
+<meta name="assert" content="box-sizing supports only the grammar 'content-box | border-box'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("box-sizing", "auto");
+test_invalid_value("box-sizing", "content-box border-box");
+test_invalid_value("box-sizing", "fill-box");
+test_invalid_value("box-sizing", "margin-box");
+test_invalid_value("box-sizing", "padding-box");
+test_invalid_value("box-sizing", "view-box");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/box-sizing-valid.html b/css/css-ui/parsing/box-sizing-valid.html
new file mode 100644
index 0000000..3d23d47
--- /dev/null
+++ b/css/css-ui/parsing/box-sizing-valid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing box-sizing with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#box-sizing">
+<meta name="assert" content="box-sizing supports the full grammar 'content-box | border-box'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("box-sizing", "content-box");
+test_valid_value("box-sizing", "border-box");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/caret-color-invalid.html b/css/css-ui/parsing/caret-color-invalid.html
new file mode 100644
index 0000000..9751b9b
--- /dev/null
+++ b/css/css-ui/parsing/caret-color-invalid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing caret-color with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#caret-color">
+<meta name="assert" content="caret-color supports only the grammar 'auto | <color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("caret-color", "none");
+test_invalid_value("caret-color", "invert");
+test_invalid_value("caret-color", "50%");
+test_invalid_value("caret-color", "red green");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/caret-color-valid.html b/css/css-ui/parsing/caret-color-valid.html
new file mode 100644
index 0000000..81cfe25
--- /dev/null
+++ b/css/css-ui/parsing/caret-color-valid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing caret-color with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#caret-color">
+<meta name="assert" content="caret-color supports the full grammar 'auto | <color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("caret-color", "auto");
+test_valid_value("caret-color", "rgba(10, 20, 30, 0.4)");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/cursor-invalid.html b/css/css-ui/parsing/cursor-invalid.html
new file mode 100644
index 0000000..bd05530
--- /dev/null
+++ b/css/css-ui/parsing/cursor-invalid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing cursor with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#cursor">
+<meta name="assert" content="cursor supports only the grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("cursor", "en-resize");
+test_invalid_value("cursor", 'url("https://example.com/") alias');
+test_invalid_value("cursor", '1 2 url("https://example.com/"), copy');
+test_invalid_value("cursor", 'url("https://example.com/"), url("https://example.com/") 3, move');
+
+test_invalid_value("cursor", 'url("https://example.com/") 1px 2px, copy');
+test_invalid_value("cursor", 'url("https://example.com/"), url("https://example.com/") 3% 4%, move');
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/cursor-valid.html b/css/css-ui/parsing/cursor-valid.html
new file mode 100644
index 0000000..20ea267
--- /dev/null
+++ b/css/css-ui/parsing/cursor-valid.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing cursor with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#cursor">
+<meta name="assert" content="cursor supports the full grammar.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("cursor", "auto");
+test_valid_value("cursor", "default");
+test_valid_value("cursor", "none");
+test_valid_value("cursor", "context-menu");
+test_valid_value("cursor", "help");
+test_valid_value("cursor", "pointer");
+test_valid_value("cursor", "progress");
+test_valid_value("cursor", "wait");
+test_valid_value("cursor", "cell");
+test_valid_value("cursor", "crosshair");
+test_valid_value("cursor", "text");
+test_valid_value("cursor", "vertical-text");
+test_valid_value("cursor", "alias");
+test_valid_value("cursor", "copy");
+test_valid_value("cursor", "move");
+test_valid_value("cursor", "no-drop");
+test_valid_value("cursor", "not-allowed");
+test_valid_value("cursor", "grab");
+test_valid_value("cursor", "grabbing");
+test_valid_value("cursor", "e-resize");
+test_valid_value("cursor", "n-resize");
+test_valid_value("cursor", "ne-resize");
+test_valid_value("cursor", "nw-resize");
+test_valid_value("cursor", "s-resize");
+test_valid_value("cursor", "se-resize");
+test_valid_value("cursor", "sw-resize");
+test_valid_value("cursor", "w-resize");
+test_valid_value("cursor", "ew-resize");
+test_valid_value("cursor", "ns-resize");
+test_valid_value("cursor", "nesw-resize");
+test_valid_value("cursor", "nwse-resize");
+test_valid_value("cursor", "col-resize");
+test_valid_value("cursor", "row-resize");
+test_valid_value("cursor", "all-scroll");
+test_valid_value("cursor", "zoom-in");
+test_valid_value("cursor", "zoom-out");
+
+test_valid_value("cursor", 'url("https://example.com/"), alias', ['url("https://example.com/"), alias', 'url(https://example.com/), alias']);
+test_valid_value("cursor", 'url("https://example.com/") 1 calc(2 + 0), copy', ['url("https://example.com/") 1 calc(2), copy', 'url("https://example.com/") 1 2, copy', 'url(https://example.com/) 1 2, copy']);
+test_valid_value("cursor", 'url("https://example.com/"), url("https://example.com/") 3 -4, move', ['url("https://example.com/"), url("https://example.com/") 3 -4, move', 'url(https://example.com/), url(https://example.com/) 3 -4, move']);
+test_valid_value("cursor", 'url("https://example.com/") 5 6, grab', ['url("https://example.com/") 5 6, grab', 'url(https://example.com/) 5 6, grab']);
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-color-invalid.html b/css/css-ui/parsing/outline-color-invalid.html
new file mode 100644
index 0000000..029c1e9
--- /dev/null
+++ b/css/css-ui/parsing/outline-color-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline-color with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline-color">
+<meta name="assert" content="outline-color supports only the grammar '<color> | invert'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("outline-color", "auto");
+test_invalid_value("outline-color", "none");
+test_invalid_value("outline-color", "50%");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-color-valid-mandatory.html b/css/css-ui/parsing/outline-color-valid-mandatory.html
new file mode 100644
index 0000000..891bbc0
--- /dev/null
+++ b/css/css-ui/parsing/outline-color-valid-mandatory.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline-color with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline-color">
+<meta name="assert" content="outline-color supports '<color>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("outline-color", "rgba(10, 20, 30, 0.4)");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-color-valid-optional.html b/css/css-ui/parsing/outline-color-valid-optional.html
new file mode 100644
index 0000000..bd3b9e6
--- /dev/null
+++ b/css/css-ui/parsing/outline-color-valid-optional.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline-color with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline-color">
+<meta name="assert" content="outline-color supports 'invert'.">
+<meta name="flags" content="may">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Conformant UAs may ignore the invert value on platforms that do not support color inversion of the pixels on the screen.
+test_valid_value("outline-color", "invert");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-invalid.html b/css/css-ui/parsing/outline-invalid.html
new file mode 100644
index 0000000..ea8d88b
--- /dev/null
+++ b/css/css-ui/parsing/outline-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline">
+<meta name="assert" content="outline supports only the grammar '<outline-color> || <outline> || <outline>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("outline", "invert solid rgba(10, 20, 30, 0.4)");
+test_invalid_value("outline", "double invert groove");
+test_invalid_value("outline", "thin outset thick");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-offset-invalid.html b/css/css-ui/parsing/outline-offset-invalid.html
new file mode 100644
index 0000000..f547160
--- /dev/null
+++ b/css/css-ui/parsing/outline-offset-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline-offset with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline-offset">
+<meta name="assert" content="outline-offset supports only the grammar '<length>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("outline-offset", "auto");
+test_invalid_value("outline-offset", "1%");
+test_invalid_value("outline-offset", "2px 3px");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-offset-valid.html b/css/css-ui/parsing/outline-offset-valid.html
new file mode 100644
index 0000000..a7064e5
--- /dev/null
+++ b/css/css-ui/parsing/outline-offset-valid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline-offset with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline-offset">
+<meta name="assert" content="outline-offset supports the full grammar '<length>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("outline-offset", "0", "0px");
+test_valid_value("outline-offset", "1px");
+test_valid_value("outline-offset", "2em");
+test_valid_value("outline-offset", "calc(3rem + 4vw)");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-style-invalid.html b/css/css-ui/parsing/outline-style-invalid.html
new file mode 100644
index 0000000..1ea6230
--- /dev/null
+++ b/css/css-ui/parsing/outline-style-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 4: parsing outline-style with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui/#outline-style">
+<meta name="assert" content="outline-style supports only the grammar 'auto | <outline-line-style>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("outline-style", "hidden");
+test_invalid_value("outline-style", "dotted dashed");
+test_invalid_value("outline-style", "solid double groove ridge");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-style-valid.html b/css/css-ui/parsing/outline-style-valid.html
new file mode 100644
index 0000000..cebf614
--- /dev/null
+++ b/css/css-ui/parsing/outline-style-valid.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 4: parsing outline-style with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui/#outline-style">
+<meta name="assert" content="outline-style supports the full grammar 'auto | <outline-line-style>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("outline-style", "auto");
+test_valid_value("outline-style", "none");
+test_valid_value("outline-style", "dotted");
+test_valid_value("outline-style", "dashed");
+test_valid_value("outline-style", "solid");
+test_valid_value("outline-style", "double");
+test_valid_value("outline-style", "groove");
+test_valid_value("outline-style", "ridge");
+test_valid_value("outline-style", "inset");
+test_valid_value("outline-style", "outset");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-valid-mandatory.html b/css/css-ui/parsing/outline-valid-mandatory.html
new file mode 100644
index 0000000..f4cfe2c
--- /dev/null
+++ b/css/css-ui/parsing/outline-valid-mandatory.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline">
+<link rel="help" href="https://drafts.csswg.org/cssom/#serializing-css-values">
+<meta name="assert" content="outline supports the full grammar '<outline-color> || <outline> || <outline>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("outline", "rgba(10, 20, 30, 0.4)");
+
+test_valid_value("outline", "auto");
+test_valid_value("outline", "none"); // Edge serializes as "invert"
+test_valid_value("outline", "dotted");
+test_valid_value("outline", "dashed");
+test_valid_value("outline", "solid");
+test_valid_value("outline", "double");
+test_valid_value("outline", "groove");
+test_valid_value("outline", "ridge");
+test_valid_value("outline", "inset");
+test_valid_value("outline", "outset");
+
+test_valid_value("outline", "0", "0px");
+test_valid_value("outline", "1px");
+test_valid_value("outline", "calc(2em + 3ex)");
+test_valid_value("outline", "thin");
+test_valid_value("outline", "medium"); // Edge serializes as "invert"
+test_valid_value("outline", "thick");
+
+test_valid_value("outline", "dashed thin");
+test_valid_value("outline", "medium rgba(10, 20, 30, 0.4)", ["rgba(10, 20, 30, 0.4) medium", "rgba(10, 20, 30, 0.4)"]);
+
+test_valid_value("outline", "3px ridge rgba(10, 20, 30, 0.4)", "rgba(10, 20, 30, 0.4) ridge 3px");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-valid-optional.html b/css/css-ui/parsing/outline-valid-optional.html
new file mode 100644
index 0000000..66ec897
--- /dev/null
+++ b/css/css-ui/parsing/outline-valid-optional.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing outline with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#outline">
+<link rel="help" href="https://drafts.csswg.org/cssom/#serializing-css-values">
+<meta name="assert" content="outline supports the full grammar '<outline-color> || <outline> || <outline>'.">
+<meta name="assert" content="outline serializes in canonical order, with shortest possible serialization.">
+<meta name="flags" content="may">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+// Conformant UAs may ignore the invert value on platforms that do not support color inversion of the pixels on the screen.
+test_valid_value("outline", "invert");
+test_valid_value("outline", "invert dotted 1px");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-width-invalid.html b/css/css-ui/parsing/outline-width-invalid.html
new file mode 100644
index 0000000..71cb533
--- /dev/null
+++ b/css/css-ui/parsing/outline-width-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 4: parsing outline-width with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui/#outline-width">
+<meta name="assert" content="outline-width supports only the grammar '<border-width>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("outline-width", "auto");
+test_invalid_value("outline-width", "1%");
+test_invalid_value("outline-width", "thin medium thick medium thin");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/outline-width-valid.html b/css/css-ui/parsing/outline-width-valid.html
new file mode 100644
index 0000000..f683991
--- /dev/null
+++ b/css/css-ui/parsing/outline-width-valid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 4: parsing outline-width with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui/#outline-width">
+<meta name="assert" content="outline-width supports the full grammar '<border-width>'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("outline-width", "0", "0px");
+test_valid_value("outline-width", "1px");
+test_valid_value("outline-width", "2em");
+test_valid_value("outline-width", "calc(2em + 3ex)");
+test_valid_value("outline-width", "thin");
+test_valid_value("outline-width", "medium");
+test_valid_value("outline-width", "thick");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/resize-invalid.html b/css/css-ui/parsing/resize-invalid.html
new file mode 100644
index 0000000..5fdb8ee
--- /dev/null
+++ b/css/css-ui/parsing/resize-invalid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing resize with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#resize">
+<meta name="assert" content="resize supports only the grammar 'none | both | horizontal | vertical'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("resize", "auto");
+test_invalid_value("resize", "horizontal vertical");
+test_invalid_value("resize", "both 0");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/resize-valid.html b/css/css-ui/parsing/resize-valid.html
new file mode 100644
index 0000000..e9f03d9
--- /dev/null
+++ b/css/css-ui/parsing/resize-valid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing resize with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#resize">
+<meta name="assert" content="resize supports the full grammar 'none | both | horizontal | vertical'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("resize", "none");
+test_valid_value("resize", "both");
+test_valid_value("resize", "horizontal");
+test_valid_value("resize", "vertical");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/resources/parsing-testcommon.js b/css/css-ui/parsing/resources/parsing-testcommon.js
new file mode 100644
index 0000000..9427f53
--- /dev/null
+++ b/css/css-ui/parsing/resources/parsing-testcommon.js
@@ -0,0 +1,39 @@
+'use strict';
+
+// serializedValue can be the expected serialization of value,
+// or an array of permitted serializations,
+// or omitted if value should serialize as value.
+function test_valid_value(property, value, serializedValue) {
+    if (arguments.length < 3)
+        serializedValue = value;
+
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_not_equals(div.style[property], "", "property should be set");
+
+        var div = document.createElement('div');
+        div.style[property] = value;
+        var readValue = div.style[property];
+        if (serializedValue instanceof Array)
+            assert_true(serializedValue.includes(readValue), "serialization should be sound");
+        else
+            assert_equals(readValue, serializedValue, "serialization should be canonical");
+
+        div.style[property] = readValue;
+        assert_equals(div.style[property], readValue, "serialization should round-trip");
+
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should set the property value");
+}
+
+function test_invalid_value(property, value) {
+    var stringifiedValue = JSON.stringify(value);
+
+    test(function(){
+        var div = document.createElement('div');
+        div.style[property] = value;
+        assert_equals(div.style[property], "");
+    }, "e.style['" + property + "'] = " + stringifiedValue + " should not set the property value");
+}
diff --git a/css/css-ui/parsing/text-overflow-invalid.html b/css/css-ui/parsing/text-overflow-invalid.html
new file mode 100644
index 0000000..f397481
--- /dev/null
+++ b/css/css-ui/parsing/text-overflow-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing text-overflow with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#text-overflow">
+<meta name="assert" content="text-overflow supports only the grammar 'clip | ellipsis'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("text-overflow", "auto");
+test_invalid_value("text-overflow", "clip ellipsis clip");
+</script>
+</body>
+</html>
diff --git a/css/css-ui/parsing/text-overflow-valid.html b/css/css-ui/parsing/text-overflow-valid.html
new file mode 100644
index 0000000..80f267e
--- /dev/null
+++ b/css/css-ui/parsing/text-overflow-valid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS UI Level 3: parsing text-overflow with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-ui-3/#text-overflow">
+<meta name="assert" content="text-overflow supports the full grammar 'clip | ellipsis'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("text-overflow", "clip");
+test_valid_value("text-overflow", "ellipsis");
+</script>
+</body>
+</html>
diff --git a/css/css-values/attr-invalid-type-001.html b/css/css-values/attr-invalid-type-001.html
index a56c144..c427ade 100644
--- a/css/css-values/attr-invalid-type-001.html
+++ b/css/css-values/attr-invalid-type-001.html
@@ -7,7 +7,7 @@
 		Attribute references (types)
 	</title>
 	<meta name="assert" content="
-		When the type of an att() function is known and unexpected, the declaration is ingored
+		When the type of an attr() function is known and unexpected, the declaration is ignored
 	" />
 
 	<link
diff --git a/css/css-values/attr-invalid-type-002.html b/css/css-values/attr-invalid-type-002.html
index 69766c0..797700d 100644
--- a/css/css-values/attr-invalid-type-002.html
+++ b/css/css-values/attr-invalid-type-002.html
@@ -7,7 +7,7 @@
 		Attribute references (types)
 	</title>
 	<meta name="assert" content="
-		When the type of an att() function is known and unexpected, the declaration is ignored
+		When the type of an attr() function is known and unexpected, the declaration is ignored
 	" />
 
 	<link
diff --git a/css/css-values/calc-in-calc.html b/css/css-values/calc-in-calc.html
index 3e035fe..51a1ae5 100644
--- a/css/css-values/calc-in-calc.html
+++ b/css/css-values/calc-in-calc.html
@@ -29,7 +29,7 @@
 
 			html { background: red; overflow: hidden; }
 			#outer { position: absolute; top: 0px; left: 0px; background: green; width: 100%; }
-			#outer { height: calc(calc(100%));
+			#outer { height: calc(calc(100%)); }
 
 	</style>
 
diff --git a/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001-ref.html b/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001-ref.html
new file mode 100644
index 0000000..0fd818c
--- /dev/null
+++ b/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001-ref.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Reftest Reference: Percentage size on orthogonal replaced elements</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<style>
+.container {
+  display: inline-block;
+  border: solid 5px black;
+  margin: 10px;
+  vertical-align: top;
+  width: 200px;
+  height: 100px;
+}
+
+img {
+  display: block;
+  width: 200px;
+  height: 100px;
+}
+
+</style>
+
+<p>The test passes if you see four filled lime rectangles with black border and <strong>no red</strong>.</p>
+
+<div class="container">
+  <img src="support/100x100-lime.png" />
+</div>
+
+<div class="container">
+  <img src="support/100x100-lime.png" />
+</div>
+
+<div class="container">
+  <img src="support/100x100-lime.png" />
+</div>
+
+<div class="container">
+  <img src="support/100x100-lime.png" />
+</div>
diff --git a/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001.html b/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001.html
new file mode 100644
index 0000000..6829920
--- /dev/null
+++ b/css/css-writing-modes/sizing-percentages-replaced-orthogonal-001.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Writing Modes Test: Percentage size on orthogonal replaced elements</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-writing-modes-3/#orthogonal-auto">
+<link rel="match" href="sizing-percentages-replaced-orthogonal-001-ref.html">
+<meta name="assert" content="Checks that orthogonal replaced elements resolve properly their percentage sizes against the expected axis from their containing block.">
+<style>
+.container {
+  display: inline-block;
+  border: solid 5px black;
+  margin: 10px;
+  vertical-align: top;
+  width: 200px;
+  height: 100px;
+  background: red;
+}
+
+img {
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+
+.horizontalTB { writing-mode: horizontal-tb; }
+.verticalLR { writing-mode: vertical-lr; }
+.verticalRL {  writing-mode: vertical-rl; }
+</style>
+
+<p>The test passes if you see four filled lime rectangles with black border and <strong>no red</strong>.</p>
+
+<div class="container horizontalTB">
+  <img class="verticalLR" src="support/100x100-lime.png" />
+</div>
+
+<div class="container horizontalTB">
+  <img class="verticalRL" src="support/100x100-lime.png" />
+</div>
+
+<div class="container verticalLR">
+  <img class="horizontalTB" src="support/100x100-lime.png" />
+</div>
+
+<div class="container verticalRL">
+  <img class="horizontalTB" src="support/100x100-lime.png" />
+</div>
diff --git a/css/cssom-view/interfaces.html b/css/cssom-view/interfaces.html
index 308c1b4..38de8dc 100644
--- a/css/cssom-view/interfaces.html
+++ b/css/cssom-view/interfaces.html
@@ -3,7 +3,7 @@
 <!-- WARNING: These tests are preliminary and probably partly incorrect.  -->
 <title>CSSOM View automated IDL tests</title>
 <link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
-<link rel="help" href="https://drafts.csswg.org/cssom-1/#idl-index">
+<link rel="help" href="https://drafts.csswg.org/cssom-view-1/#idl-index">
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script src=/resources/WebIDLParser.js></script>
@@ -17,12 +17,18 @@
 <script>
 "use strict";
 
-function doTest([html, dom, uievents, cssom, geometry, cssom_view]) {
+function doTest([html, dom, uievents, cssom, cssom_view]) {
 
   var idlArray = new IdlArray();
   var svg = "interface SVGElement : Element {};";
-  idlArray.add_untested_idls(html + dom + svg + cssom + geometry);
-  idlArray.add_untested_idls(uievents, { only: ['UIEvent', 'UIEventInit', 'MouseEvent', 'MouseEventInit', 'EventModifierInit'] });
+  idlArray.add_untested_idls(html + dom + svg + cssom);
+  idlArray.add_untested_idls(uievents, { only: [
+    'UIEvent',
+    'UIEventInit',
+    'MouseEvent',
+    'MouseEventInit',
+    'EventModifierInit']
+  });
   idlArray.add_idls(cssom_view);
 
   idlArray.add_objects({
@@ -39,7 +45,6 @@
     // "MouseEvent": ["new MouseEvent('foo')"],
     "Text": ["document.createTextNode('x')"],
     // "CSSPseudoElement": [],
-    "Document": ["document"],
   });
   idlArray.test();
 };
@@ -60,7 +65,6 @@
                       fetchData("/interfaces/dom.idl"),
                       fetchData("/interfaces/uievents.idl"),
                       fetchData("/interfaces/cssom.idl"),
-                      fetchData("/interfaces/geometry.idl"),
                       fetchData("/interfaces/cssom-view.idl"),
                       waitForLoad()])
                 .then(doTest);
diff --git a/css/cssom-view/scroll-behavior-smooth.html b/css/cssom-view/scroll-behavior-smooth.html
index f144a5f..e1a7a6a 100644
--- a/css/cssom-view/scroll-behavior-smooth.html
+++ b/css/cssom-view/scroll-behavior-smooth.html
@@ -56,5 +56,85 @@
     window.scrollTo(0, 0);
   }, "BODY element scroll-behavior should not propagate to viewport");
 
+  var instantHistoryNavigationTest =
+    async_test("Instant scrolling while doing history navigation.");
+  var smoothHistoryNavigationTest =
+    async_test("Smooth scrolling while doing history navigation.");
+
+  function instant() {
+    document.documentElement.className = "";
+    document.body.className = "";
+    window.scrollTo(0, 0);
+    var p = document.createElement("pre");
+    p.textContent = new Array(1000).join("newline\n");
+    var a = document.createElement("a");
+    a.href = "#";
+    a.name = "foo";
+    a.textContent = "foo";
+    p.appendChild(a);
+    document.body.appendChild(p);
+    window.onhashchange = function() {
+      window.onhashchange = function() {
+        instantHistoryNavigationTest.step(function() {
+          assert_equals(location.hash, "", "Shouldn't be scrolled to a fragment.");
+          assert_equals(window.scrollY, 0, "Shouldn't be scrolled back to top yet.");
+        });
+        p.remove();
+        instantHistoryNavigationTest.done();
+        smooth();
+      }
+
+      instantHistoryNavigationTest.step(function() {
+        assert_equals(location.hash, "#foo", "Should be scrolled to a fragment.");
+        assert_not_equals(window.scrollY, 0, "Shouldn't be scrolled to top anymore.");
+      });
+      history.back();
+    }
+
+    instantHistoryNavigationTest.step(function() {
+      assert_equals(window.scrollY, 0, "Should be scrolled to top.");
+      assert_equals(location.hash, "", "Shouldn't be scrolled to a fragment.");
+    });
+    location.hash = "foo";
+  };
+  instant();
+
+  function smooth() {
+    document.documentElement.className = "";
+    document.body.className = "";
+    window.scrollTo(0, 0);
+    var p = document.createElement("pre");
+    p.textContent = new Array(1000).join("newline\n");
+    var a = document.createElement("a");
+    a.href = "#";
+    a.name = "bar";
+    a.textContent = "bar";
+    p.appendChild(a);
+    document.body.appendChild(p);
+    window.onhashchange = function() {
+      window.onhashchange = function() {
+        smoothHistoryNavigationTest.step(function() {
+          assert_equals(location.hash, "", "Shouldn't be scrolled to a fragment.");
+          assert_not_equals(window.scrollY, 0, "Shouldn't be scrolled back to top yet.");
+        });
+        p.remove();
+        smoothHistoryNavigationTest.done();
+      }
+
+      smoothHistoryNavigationTest.step(function() {
+        assert_equals(location.hash, "#bar", "Should be scrolled to a fragment.");
+        assert_not_equals(window.scrollY, 0, "Shouldn't be scrolled to top anymore.");
+      });
+      history.back();
+    }
+
+    smoothHistoryNavigationTest.step(function() {
+      assert_equals(window.scrollY, 0, "Should be scrolled to top.");
+      assert_equals(location.hash, "", "Shouldn't be scrolled to a fragment.");
+    });
+    location.hash = "bar";
+    document.documentElement.className = "smooth";
+  };
+
   testContainer.style.display = "none";
 </script>
diff --git a/css/cssom-view/scrollIntoView-shadow.html b/css/cssom-view/scrollIntoView-shadow.html
index 6d44df6..eb1bce3 100644
--- a/css/cssom-view/scrollIntoView-shadow.html
+++ b/css/cssom-view/scrollIntoView-shadow.html
@@ -14,7 +14,7 @@
 
 test(t => {
   var shadow = document.getElementById("shadow");
-  var shadowRoot = shadow.createShadowRoot();
+  var shadowRoot = shadow.attachShadow({ mode: "open" });
   var shadowDiv = document.createElement("div");
   shadowDiv.style.height = "200px";
   shadowDiv.style.width = "200px";
@@ -30,4 +30,4 @@
   assert_approx_equals(window.scrollX, expected_x, 1);
   assert_approx_equals(window.scrollY, expected_y, 1);
 }, "scrollIntoView should behave correctly if applies to shadow dom elements");
-</script>
\ No newline at end of file
+</script>
diff --git a/css/cssom/CSSStyleRule-set-selectorText-namespace.html b/css/cssom/CSSStyleRule-set-selectorText-namespace.html
new file mode 100644
index 0000000..4da0a33
--- /dev/null
+++ b/css/cssom/CSSStyleRule-set-selectorText-namespace.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>CSSOM StyleRule selectorText property setter with namespaces</title>
+<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<style type="text/css" id="styleElement">
+@namespace url(http://www.w3.org/1999/xhtml);
+@namespace svg url(http://www.w3.org/2000/svg);
+
+svg|*.style0 { background-color: rgb(0, 0, 255) !important; }
+svg|*.style1 { background-color: rgb(255, 0, 255); }
+</style>
+
+<span>
+  <p></p>
+
+  <svg height="30" width="200" id="container" class="style1" lang="zh-CN" language segment="42 43">
+    <text x="0" y="15">SVG text</text>
+  </svg>
+</span>
+
+<script>
+  var styleSheet = document.getElementById("styleElement").sheet;
+  var rule = styleSheet.cssRules[2];
+
+  var divContainerStyle = getComputedStyle(document.getElementById("container"));
+
+  const originalStyleSelector = "svg|*.style0";
+
+  var assertColors = function(selectorMatches) {
+    assert_equals(divContainerStyle.getPropertyValue('background-color'), selectorMatches ? "rgb(0, 0, 255)" : "rgb(255, 0, 255)")
+  };
+
+  [
+    {selector: ".style1", isMatch: false, },
+    {selector: "svg|*.style1  ", isMatch: true, normalizedSelector: "svg|*.style1"},
+    {selector: "*|*.style1  ", isMatch: true, normalizedSelector: "*|*.style1"},
+    {selector: " *.style1  ", isMatch: false, normalizedSelector: ".style1"},
+    {selector: "p", isMatch: false},
+  ].forEach(function(testCase) {
+    test(function() {
+      // Check if starting with the default value.
+      assert_equals(rule.selectorText, originalStyleSelector);
+
+      this.add_cleanup(function() { rule.selectorText = originalStyleSelector; });
+
+      assertColors(false);
+
+      rule.selectorText = testCase.selector;
+
+      var expectedSelector = testCase.normalizedSelector ? testCase.normalizedSelector : testCase.selector;
+
+      assert_equals(rule.selectorText, expectedSelector);
+
+      assertColors(testCase.isMatch);
+    }, "CSSStyleRule: selectorText value: |" + testCase.selector + "| isMatch: " + testCase.isMatch);
+  });
+</script>
diff --git a/css/cssom/CSSStyleRule-set-selectorText.html b/css/cssom/CSSStyleRule-set-selectorText.html
new file mode 100644
index 0000000..e29db52
--- /dev/null
+++ b/css/cssom/CSSStyleRule-set-selectorText.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>CSSOM StyleRule selectorText property setter</title>
+<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<style type="text/css" id="styleElement">
+  .style0 { background-color: rgb(0, 0, 255) !important; }
+  .style1 { background-color: rgb(255, 0, 255); }
+</style>
+
+<span>
+  <p></p>
+  <div id="container" class="style1" lang="zh-CN" language segment="42 43">
+  </div>
+</span>
+
+<script>
+  var styleSheet = document.getElementById("styleElement").sheet;
+  var rule = styleSheet.cssRules[0];
+
+  var divContainerStyle = getComputedStyle(document.getElementById("container"));
+
+  const originalStyleSelector = ".style0";
+
+  var assertColors = function(selectorMatches) {
+    assert_equals(divContainerStyle.backgroundColor, selectorMatches ? "rgb(0, 0, 255)" : "rgb(255, 0, 255)")
+  };
+
+  test(function() {
+    assert_equals(typeof rule.selectorText, "string");
+    assert_equals(rule.selectorText, originalStyleSelector);
+  }, "CSSStyleRule: Can read selectorText value.");
+
+  [ // Invalid selector values.
+    "",
+    " ",
+    "!!",
+    "123",
+    "-",
+    "$",
+    ":",
+    "::",
+    ":::",
+    "::gibberish",
+    ":gibberish",
+    ".",
+    "#",
+    "[]",
+    "[",
+    "()",
+    "(",
+    "{}",
+    "{",
+  ].forEach(function(selector) {
+    test(function() {
+      assert_equals(rule.selectorText, originalStyleSelector);
+
+      this.add_cleanup(function() { rule.selectorText = originalStyleSelector; });
+
+      rule.selectorText = selector;
+
+      assert_equals(rule.selectorText, originalStyleSelector);
+    }, "CSSStyleRule: Invalid CSS selector: " + selector);
+  });
+
+
+  [ // Valid selector values.
+    {selector: "#container", isMatch: true},
+    {selector: "#container  ", isMatch: true, normalizedSelector: "#container"},
+    {selector: "  #container ", isMatch: true, normalizedSelector: "#container"},
+    {selector: ".style1", isMatch: true},
+    {selector: "div.style1", isMatch: true},
+    {selector: "div:not(#non-existing-id)", isMatch: true},
+    {selector: "div", isMatch: true},
+    {selector: "*", isMatch: true},
+
+    {selector: "#no-match", isMatch: false},
+    {selector: "ÇĞıİ", isMatch: false},
+    {selector: "🤓", isMatch: false},
+
+    {selector: "[language]", isMatch: true},
+    {selector: "[language-no]", isMatch: false},
+    {selector: "[lang=\"zh-CN\"]", isMatch: true},
+    {selector: "[lang=\"ab-CD\"]", isMatch: false},
+    {selector: "[segment~=\"43\"]", isMatch: true},
+    {selector: "[segment~=\"42\"]", isMatch: true},
+    {selector: "[lang|=\"zh\"]", isMatch: true},
+    {selector: "[lang|=\"zh-CN\"]", isMatch: true},
+    {selector: "[lang|=\"ab\"]", isMatch: false},
+    {selector: "[lang|=\"z\"]", isMatch: false},
+    {selector: "[lang^=\"z\"]", isMatch: true},
+    {selector: "[lang^=\"ab\"]", isMatch: false},
+    {selector: "[segment$=\"43\"]", isMatch: true},
+    {selector: "[segment$=\"3\"]", isMatch: true},
+    {selector: "[segment$=\"42\"]", isMatch: false},
+    {selector: "[lang*=\"-\"]", isMatch: true},
+    {selector: "[lang*=\"h-\"]", isMatch: true},
+    {selector: "[lang*=\"ab\"]", isMatch: false},
+
+    {selector: "*|div", isMatch: true, normalizedSelector: "div"},
+    {selector: "|div", isMatch: false},
+    {selector: "*|a", isMatch: false, normalizedSelector: "a"},
+    {selector: "*|*", isMatch: true, normalizedSelector: "*"},
+    {selector: "[*|lang]", isMatch: true, normalizedSelector: "[*|lang]"},
+    {selector: "[|lang]", isMatch: true, normalizedSelector: "[lang]"},
+
+    {selector: ":active", isMatch: false},
+    {selector: ":not(:active)", isMatch: true},
+    {selector: "*:not(:active)", isMatch: true, normalizedSelector: ":not(:active)"},
+    {selector: "div:not(:active)", isMatch: true},
+    {selector: "div:active", isMatch: false},
+
+    {selector: "span div", isMatch: true},
+    {selector: "span  div  ", isMatch: true, normalizedSelector: "span div"},
+    {selector: "span > div", isMatch: true},
+    {selector: "div div", isMatch: false},
+    {selector: "div > div", isMatch: false},
+    {selector: "p + div", isMatch: true},
+    {selector: "span + div", isMatch: false},
+    {selector: "p ~ div", isMatch: true},
+    {selector: "span ~ div", isMatch: false},
+
+    {selector: ":lang(zh-CN)", isMatch: true},
+    {selector: ":lang(zh)", isMatch: true},
+    {selector: ":lang(tr-AZ)", isMatch: false},
+
+    {selector: "::after", isMatch: false, normalizedSelector: "::after"},
+    {selector: ":after", isMatch: false, normalizedSelector: "::after"},
+    {selector: "::before", isMatch: false, normalizedSelector: "::before"},
+    {selector: ":before", isMatch: false, normalizedSelector: "::before"},
+    {selector: "::first-letter", isMatch: false, normalizedSelector: "::first-letter"},
+    {selector: ":first-letter", isMatch: false, normalizedSelector: "::first-letter"},
+    {selector: "::first-line", isMatch: false, normalizedSelector: "::first-line"},
+    {selector: ":first-line", isMatch: false, normalizedSelector: "::first-line"},
+
+    {selector: "div:focus:not([lang=\"zh-CN\"])", isMatch: false},
+    {selector: "div[lang=\"zh-CN\"]:not(:focus)", isMatch: true},
+  ].forEach(function(testCase) {
+    test(function() {
+      // Check if starting with the default value.
+      assert_equals(rule.selectorText, originalStyleSelector);
+
+      this.add_cleanup(function() { rule.selectorText = originalStyleSelector; });
+
+      assertColors(false);
+
+      rule.selectorText = testCase.selector;
+
+      var expectedSelector = testCase.normalizedSelector ? testCase.normalizedSelector : testCase.selector;
+
+      assert_equals(rule.selectorText, expectedSelector);
+
+      assertColors(testCase.isMatch);
+    }, "CSSStyleRule: selectorText value: |" + testCase.selector + "| isMatch: " + testCase.isMatch);
+  });
+</script>
diff --git a/css/cssom/css-style-attribute-modifications.html b/css/cssom/css-style-attribute-modifications.html
index 6177c33..524a5ad 100644
--- a/css/cssom/css-style-attribute-modifications.html
+++ b/css/cssom/css-style-attribute-modifications.html
@@ -9,5 +9,8 @@
   var el = document.getElementById("test");
   el.style.color = "";
   assert_true(el.hasAttribute("style"));
+
+  el.removeAttribute("style");
+  assert_false(el.hasAttribute("style"));
 }, "Mutating the style declaration doesn't remove the style attribute");
 </script>
diff --git a/css/cssom/cssstyledeclaration-mutationrecord-001.html b/css/cssom/cssstyledeclaration-mutationrecord-001.html
new file mode 100644
index 0000000..5bd8456
--- /dev/null
+++ b/css/cssom/cssstyledeclaration-mutationrecord-001.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSSOM: CSSStyleDeclaration.setPropertyValue queues a mutation record when not actually mutated</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="help" href="https://drafts.csswg.org/cssom/#update-style-attribute-for">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+  document.documentElement.style.top = "0px";
+
+  let test = async_test("CSSStyleDeclaration.setPropertyValue queues a mutation record, even if not mutated");
+  let m = new MutationObserver(function(r) {
+    assert_equals(r.length, 1);
+    test.done();
+  });
+
+  m.observe(document.documentElement,  { attributes: true });
+  document.documentElement.style.top = "0px";
+</script>
diff --git a/css/cssom/cssstyledeclaration-mutationrecord-002.html b/css/cssom/cssstyledeclaration-mutationrecord-002.html
new file mode 100644
index 0000000..1cdb41a
--- /dev/null
+++ b/css/cssom/cssstyledeclaration-mutationrecord-002.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+  let test = async_test("CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values");
+  let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
+  m.observe(document.documentElement,  { attributes: true });
+
+  document.documentElement.style.width = "-100px";
+  requestAnimationFrame(() => test.done());
+</script>
diff --git a/css/cssom/cssstyledeclaration-mutationrecord-003.html b/css/cssom/cssstyledeclaration-mutationrecord-003.html
new file mode 100644
index 0000000..7a99dfc
--- /dev/null
+++ b/css/cssom/cssstyledeclaration-mutationrecord-003.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSSOM: CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed, invoked from setPropertyValue</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+  let test = async_test("CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed, invoked from setPropertyValue");
+  document.documentElement.style.top = "0";
+  let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
+  m.observe(document.documentElement,  { attributes: true });
+
+  document.documentElement.style.width = "";
+  requestAnimationFrame(() => test.done());
+</script>
diff --git a/css/cssom/cssstyledeclaration-mutationrecord-004.html b/css/cssom/cssstyledeclaration-mutationrecord-004.html
new file mode 100644
index 0000000..55956df
--- /dev/null
+++ b/css/cssom/cssstyledeclaration-mutationrecord-004.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSSOM: CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+  let test = async_test("CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed");
+  document.documentElement.style.top = "0";
+  let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
+  m.observe(document.documentElement,  { attributes: true });
+
+  document.documentElement.style.removeProperty("width");
+  requestAnimationFrame(() => test.done());
+</script>
diff --git a/css/cssom/getComputedStyle-detached-subtree.html b/css/cssom/getComputedStyle-detached-subtree.html
new file mode 100644
index 0000000..3b59ccb
--- /dev/null
+++ b/css/cssom/getComputedStyle-detached-subtree.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSSOM: getComputedStyle returns no style for elements not in the tree</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id="host">
+  <div id="non-slotted">
+    <div id="non-slotted-descendant"></div>
+  </div>
+</div>
+<iframe srcdoc="<html></html>" style="display: none"></iframe>
+<script>
+function testNoComputedStyle(element, description) {
+  test(function() {
+    assert_true(!!element);
+    let style = getComputedStyle(element);
+    assert_true(!!style);
+    assert_equals(style.length, 0);
+    assert_equals(style.color, "");
+  }, `getComputedStyle returns no style for ${description}`);
+}
+
+let detached = document.createElement('div');
+testNoComputedStyle(detached, "detached element");
+
+testNoComputedStyle(document.querySelector('iframe').contentDocument.documentElement,
+                    "element in non-rendered iframe (display: none)");
+
+host.attachShadow({ mode: "open" });
+testNoComputedStyle(document.getElementById('non-slotted'),
+                    "element outside the flat tree");
+
+testNoComputedStyle(document.getElementById('non-slotted-descendant'),
+                    "descendant outside the flat tree");
+
+let shadowRoot = detached.attachShadow({ mode: "open" });
+shadowRoot.innerHTML = `
+  <div id="detached-shadow-tree-descendant"></div>
+`;
+testNoComputedStyle(shadowRoot.getElementById('detached-shadow-tree-descendant'),
+                    "shadow tree outside of flattened tree");
+</script>
diff --git a/css/cssom/getComputedStyle-dynamic-subdoc.html b/css/cssom/getComputedStyle-dynamic-subdoc.html
index 13bd694..aa49dc3 100644
--- a/css/cssom/getComputedStyle-dynamic-subdoc.html
+++ b/css/cssom/getComputedStyle-dynamic-subdoc.html
@@ -5,10 +5,6 @@
 <link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
-<!--
-  NOTE: The way this test is written doesn't match the spec, but matches all
-  implementations, see https://github.com/w3c/csswg-drafts/issues/2403
--->
 <iframe id="frm" style="width: 100px; height: 100px"></iframe>
 <script>
 test(function() {
diff --git a/css/cssom/getComputedStyle-pseudo.html b/css/cssom/getComputedStyle-pseudo.html
index ec7a863..f5a637a 100644
--- a/css/cssom/getComputedStyle-pseudo.html
+++ b/css/cssom/getComputedStyle-pseudo.html
@@ -109,4 +109,9 @@
                   "display: contents in " + pseudo + " should reflect other non-inherited properties in CSSOM");
   });
 }, "display: contents on pseudo-elements");
+test(function() {
+  var div = document.getElementById('test');
+  assert_throws(new TypeError(), () => getComputedStyle(div, "totallynotapseudo"),
+                "getComputedStyle with an unknown pseudo-element throws");
+}, "Unknown pseudo-elements throw");
 </script>
diff --git a/css/cssom/interfaces.html b/css/cssom/interfaces.html
index b92da9f..08d04c07 100644
--- a/css/cssom/interfaces.html
+++ b/css/cssom/interfaces.html
@@ -39,7 +39,13 @@
   var idlArray = new IdlArray();
   var svg = "interface SVGElement : Element {};";
   idlArray.add_untested_idls(html + dom + svg);
-  idlArray.add_untested_idls(uievents, { only: ['UIEvent', 'UIEventInit', 'MouseEvent', 'MouseEventInit', 'EventModifierInit']});
+  idlArray.add_untested_idls(uievents, { only: [
+    'UIEvent',
+    'UIEventInit',
+    'MouseEvent',
+    'MouseEventInit',
+    'EventModifierInit'
+  ]});
   idlArray.add_idls(cssom);
 
   idlArray.add_objects({
diff --git a/css/cssom/overflow-serialization.html b/css/cssom/overflow-serialization.html
index 7e2d176..136b8ab 100644
--- a/css/cssom/overflow-serialization.html
+++ b/css/cssom/overflow-serialization.html
@@ -2,7 +2,7 @@
 <html>
 <head>
     <meta charset="utf-8">
-    <title>CSSOM - Overlow property has different serialization than other shorthands.</title>
+    <title>CSSOM - Overflow shorthand serialization</title>
     <link rel="help" href="https://drafts.csswg.org/cssom/#serialize-a-css-value">
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
@@ -11,7 +11,7 @@
         div { overflow: hidden; }
         div { overflow-x: initial; overflow-y: initial; }
         div { overflow-x: scroll; overflow-y: scroll; }
-        div { overflow-x: inherit; overflow-y: unset; }
+        div { overflow-x: scroll; overflow-y: hidden; }
     </style>
 
     <script>
@@ -22,7 +22,7 @@
         assert_equals(styleSheet.cssRules[1].style.cssText, "overflow: hidden;", "Single value overflow with non-CSS-wide keyword should serialize correctly.");
         assert_equals(styleSheet.cssRules[2].style.cssText, "overflow: initial;", "Overflow-x/y longhands with same CSS-wide keyword should serialize correctly.");
         assert_equals(styleSheet.cssRules[3].style.cssText, "overflow: scroll;", "Overflow-x/y longhands with same non-CSS-wide keyword should serialize correctly.");
-        assert_equals(styleSheet.cssRules[4].style.cssText, "overflow-x: inherit; overflow-y: unset;", "Overflow-x/y longhands with different keywords should serialize correctly.");
+        assert_equals(styleSheet.cssRules[4].style.cssText, "overflow: scroll hidden;", "Overflow-x/y longhands with different keywords should serialize correctly.");
 
         var div = document.createElement('div');
         div.style.overflow = "inherit";
@@ -40,9 +40,9 @@
         div.style.overflowY = "scroll";
         assert_equals(div.style.overflow, "scroll", "Overflow-x/y longhands with same non-CSS-wide keyword should serialize correctly.");
 
-        div.style.overflowX = "inherit";
-        div.style.overflowY = "unset";
-        assert_equals(div.style.overflow, "", "Overflow-x/y longhands with different keywords shouldn't serialize.");
+        div.style.overflowX = "scroll";
+        div.style.overflowY = "hidden";
+        assert_equals(div.style.overflow, "scroll hidden", "Overflow-x/y longhands with different keywords should serialize correctly.");
     });
     </script>
 </head>
diff --git a/css/cssom/shorthand-values.html b/css/cssom/shorthand-values.html
index eb756b5..d8d7f53 100644
--- a/css/cssom/shorthand-values.html
+++ b/css/cssom/shorthand-values.html
@@ -32,7 +32,7 @@
         'border: 1px; border-top-color: red;': 'border-width: 1px; border-top-color: red;',
         'border: solid; border-style: dotted': 'border: dotted;',
         'border-width: 1px;': 'border-width: 1px;',
-        'overflow-x: scroll; overflow-y: hidden;': 'overflow-x: scroll; overflow-y: hidden;',
+        'overflow-x: scroll; overflow-y: hidden;': 'overflow: scroll hidden;',
         'overflow-x: scroll; overflow-y: scroll;': 'overflow: scroll;',
         'outline-width: 2px; outline-style: dotted; outline-color: blue;': 'outline: blue dotted 2px;',
         'margin-top: 1px; margin-right: 2px; margin-bottom: 3px; margin-left: 4px;': 'margin: 1px 2px 3px 4px;',
diff --git a/css/geometry/DOMPoint-001.html b/css/geometry/DOMPoint-001.html
index 5aae0b0..0554d3f 100644
--- a/css/geometry/DOMPoint-001.html
+++ b/css/geometry/DOMPoint-001.html
@@ -21,7 +21,7 @@
             checkDOMPoint(new DOMPoint(), {x:0, y:0, z:0, w:1});
         },'testConstructor0');
         test(function() {
-            assert_throws(new TypeError(), function() { new DOMPoint(1);})
+            checkDOMPoint(new DOMPoint(1), {x:1, y:0, z:0, w:1});
         },'testConstructor1');
         test(function() {
             checkDOMPoint(new DOMPoint(1, 2), {x:1, y:2, z:0, w:1});
@@ -72,11 +72,11 @@
             checkDOMPoint(new DOMPoint({x:"a", y:"b"}), {x:NaN, y:NaN, z:0, w:1});
         },'testConstructorUndefined2');
         test(function() {
-            assert_throws(new TypeError(), function() { new DOMPointReadOnly();})
-        },'testConstructorIllegal1');
+            checkDOMPoint(new DOMPointReadOnly(), {x:0, y:0, z:0, w:1});
+        },'DOMPointReadOnly constructor with no values');
         test(function() {
-            assert_throws(new TypeError(), function() { new DOMPointReadOnly(1, 2, 3, 4);})
-        },'testConstructorIllegal2');
+            checkDOMPoint(new DOMPointReadOnly(1, 2, 3, 4), {x:1, y:2, z:3, w:4});
+        },'DOMPointReadOnly constructor with 4 values');
         test(function() {
             var p = new DOMPoint(0, 0, 0, 1);
             p.x = undefined;
diff --git a/css/geometry/DOMQuad-001.html b/css/geometry/DOMQuad-001.html
index fa8cc6e..4cafa02 100644
--- a/css/geometry/DOMQuad-001.html
+++ b/css/geometry/DOMQuad-001.html
@@ -44,31 +44,31 @@
         },'testConstructor4');
 
         checkDOMQuad(
-                function() { return new DOMQuad(new DOMRect(10, 20, 100, 200)); },
+                function() { return DOMQuad.fromRect(new DOMRect(10, 20, 100, 200)); },
                 {   p1: { x: 10, y: 20, z: 0, w: 1 },
                     p2: { x: 110, y: 20, z: 0, w: 1 },
                     p3: { x: 110, y: 220, z: 0, w: 1 },
                     p4: { x: 10, y: 220, z: 0, w: 1 },
                     bounds: { x: 10, y: 20, width: 100, height: 200 } },
-                'testConstructor5');
+                'fromRect() method on DOMQuad');
 
         checkDOMQuad(
-                function() { return new DOMQuad(new DOMRect(10, 20, -100, -200)) },
+                function() { return DOMQuad.fromRect(new DOMRect(10, 20, -100, -200)) },
                 {   p1: { x: 10, y: 20, z: 0, w: 1 },
                     p2: { x: -90, y: 20, z: 0, w: 1 },
                     p3: { x: -90, y: -180, z: 0, w: 1 },
                     p4: { x: 10, y: -180, z: 0, w: 1 },
                     bounds: { x: -90, y: -180, width: 100, height: 200 } },
-                'testConstructor6');
+                'fromRect() method on DOMQuad with negatives');
 
         checkDOMQuad(
-                function() { return new DOMQuad(new DOMRect(-Infinity, -Infinity, Infinity, Infinity)) },
+                function() { return DOMQuad.fromRect(new DOMRect(-Infinity, -Infinity, Infinity, Infinity)) },
                 {   p1: { x: -Infinity, y: -Infinity, z: 0, w: 1 },
                     p2: { x: NaN, y: -Infinity, z: 0, w: 1 },
                     p3: { x: NaN, y: NaN, z: 0, w: 1 },
                     p4: { x: -Infinity, y: NaN, z: 0, w: 1 },
                     bounds: { x: -Infinity, y: -Infinity, width: NaN, height: NaN } },
-                'testConstructor7');
+                'fromRect() method on DOMQuad with Infinity');
 
         checkDOMQuad(function() { return new DOMQuad(new DOMRect()); }, initial, 'testConstructor8');
 
@@ -120,24 +120,9 @@
             p2: { x: 2, y: 0, z: 0, w: 1 },
             p3: { x: 2, y: 0, z: 0, w: 1 },
             p4: { x: 2, y: 0, z: 0, w: 1 },
-            bounds: { x: 2, y: 0, width: 0, height: 0 } },
+            bounds: { x: 0, y: 0, width: 0, height: 0 } },
         'p1Top4Attributes1');
 
-        checkDOMQuad(function() {
-            var q = new DOMQuad({}, {}, {}, {});
-            q.bounds = new DOMRect(10, 10, 100, 100);
-            return q;
-        }, initial, 'boundsAttribute0');
-
-        checkDOMQuad(function() {
-            var q = new DOMQuad({}, {}, {}, {});
-            q.bounds.x = 10;
-            q.bounds.y = 10;
-            q.bounds.width = 100;
-            q.bounds.height = 100;
-            return q;
-        }, initial, 'boundsAttribute1');
-
         function checkDOMQuad(createQuad, exp, name) {
             test(function() {
                 var q = createQuad();
@@ -161,10 +146,10 @@
 
             test(function() {
                 var q = createQuad();
-                assert_equals(q.bounds.x, exp.bounds.x, "Expected value for bounds.x is " + exp.bounds.x);
-                assert_equals(q.bounds.y, exp.bounds.y, "Expected value for bounds.y is " + exp.bounds.y);
-                assert_equals(q.bounds.width, exp.bounds.width, "Expected value for bounds.width is " + exp.bounds.width);
-                assert_equals(q.bounds.height, exp.bounds.height, "Expected value for bounds.height is " + exp.bounds.height);
+                assert_equals(q.getBounds().x, exp.bounds.x, "Expected value for getBounds().x is " + exp.bounds.x);
+                assert_equals(q.getBounds().y, exp.bounds.y, "Expected value for getBounds().y is " + exp.bounds.y);
+                assert_equals(q.getBounds().width, exp.bounds.width, "Expected value for getBounds().width is " + exp.bounds.width);
+                assert_equals(q.getBounds().height, exp.bounds.height, "Expected value for getBounds().height is " + exp.bounds.height);
             }, name + ": bounds");
         }
     </script>
diff --git a/css/geometry/DOMRectList.html b/css/geometry/DOMRectList.html
index f128a05..f3d050c 100644
--- a/css/geometry/DOMRectList.html
+++ b/css/geometry/DOMRectList.html
@@ -10,12 +10,12 @@
 });
 
 test(() => {
-  assert_false('DOMRectList' in window);
-}, 'DOMRectList [NoInterfaceObject]');
+  assert_true('DOMRectList' in window);
+}, 'DOMRectList is not [NoInterfaceObject]');
 
 test(() => {
-  assert_true(domRectList instanceof Array);
-}, 'DOMRectList [LegacyArrayClass]');
+  assert_false(domRectList instanceof Array);
+}, 'DOMRectList is not [LegacyArrayClass]');
 
 test(() => {
   assert_equals(domRectList.length, 1);
diff --git a/css/geometry/historical.html b/css/geometry/historical.html
index 357ae91..4f8e65e 100644
--- a/css/geometry/historical.html
+++ b/css/geometry/historical.html
@@ -21,6 +21,8 @@
   ['DOMMatrix', 'rotateAxisAngleBy'],
   ['DOMMatrix', 'skewXBy'],
   ['DOMMatrix', 'skewYBy'],
+  // https://github.com/w3c/fxtf-drafts/commit/555a8c0beb1b7b809ccebd861a0352df31530b56
+  ['DOMQuad', 'bounds'],
 ].forEach(([interf, member]) => {
   test(() => {
     assert_true(interf in self, `${interf} should exist`);
diff --git a/css/motion/OWNERS b/css/motion/OWNERS
new file mode 100644
index 0000000..ac8ec26
--- /dev/null
+++ b/css/motion/OWNERS
@@ -0,0 +1,3 @@
+@dirkschulze
+@jihyerish
+@ewilligers
diff --git a/css/selectors/child-indexed-pseudo-class.html b/css/selectors/child-indexed-pseudo-class.html
index 06c3094..2e496d8 100644
--- a/css/selectors/child-indexed-pseudo-class.html
+++ b/css/selectors/child-indexed-pseudo-class.html
@@ -6,22 +6,31 @@
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
-var check = function(element, selectors) {
+var check = function(element, selectors, qsRoot) {
   for (var i = 0; i < selectors.length; ++i) {
     var selector = selectors[i][0];
     var expected = selectors[i][1];
     test(function() {
       assert_equals(expected, element.matches(selector));
+
+      if (qsRoot) {
+        assert_equals(expected, element === qsRoot.querySelector(selector));
+        var qsa = qsRoot.querySelectorAll(selector);
+        assert_equals(expected, !!qsa.length && element === qsa[0]);
+      }
     }, "Expected " + element.tagName + " element to " +
-         (expected ? "match " : "not match ") + selector);
+         (expected ? "match " : "not match ") + selector + " with matches" +
+         (qsRoot ? ", querySelector(), and querySelectorAll()" : ""));
   }
 }
 
 var rootOfSubtreeSelectors = [
   [ ":first-child", true ],
   [ ":last-child", true ],
+  [ ":only-child", true ],
   [ ":first-of-type", true ],
   [ ":last-of-type", true ],
+  [ ":only-of-type", true ],
   [ ":nth-child(1)", true ],
   [ ":nth-child(n)", true ],
   [ ":nth-last-child(1)", true ],
@@ -36,6 +45,11 @@
   [ ":nth-last-of-type(2)", false],
 ];
 
-check(document.documentElement, rootOfSubtreeSelectors);
+check(document.documentElement, rootOfSubtreeSelectors, document);
 check(document.createElement('div'), rootOfSubtreeSelectors);
+
+var fragment = document.createDocumentFragment();
+var div = document.createElement('div');
+fragment.appendChild(div);
+check(div, rootOfSubtreeSelectors, fragment);
 </script>
diff --git a/css/vendor-imports/mozilla/OWNERS b/css/vendor-imports/mozilla/OWNERS
index 62886bc..cf1f9b5 100644
--- a/css/vendor-imports/mozilla/OWNERS
+++ b/css/vendor-imports/mozilla/OWNERS
@@ -1 +1,2 @@
 @dbaron
+@Ms2ger
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001-ref.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001-ref.html
new file mode 100644
index 0000000..ed0cc57
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001-ref.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test Reference</title>
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<style>
+div {
+  width: 100px;
+  height: 100px;
+  background: green;
+}
+</style>
+<p>Test passes if you see a green 100px x 100px square, and no red</p>
+<div></div>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001.html
new file mode 100644
index 0000000..5bff614
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-001.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: nested flex containers with height established by 'min-height'</title>
+<link rel="match" href="flexbox-definite-sizes-001-ref.html">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#definite-sizes">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1449326">
+<style>
+div {
+  display: flex;
+}
+
+.item {
+  width: 100px;
+  background: red;
+  align-items: center;
+}
+
+.item span {
+  min-height: 100%;
+  width: 100%;
+  background: green;
+}
+</style>
+<p>Test passes if you see a green 100px x 100px square, and no red</p>
+<div style="min-height: 100px;">
+  <div class="item">
+    <span></span>
+  </div>
+</div>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-002.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-002.html
new file mode 100644
index 0000000..126fd5e
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-002.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: nested flex containers with height established by 'min-height'</title>
+<link rel="match" href="flexbox-definite-sizes-001-ref.html">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#definite-sizes">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1449326">
+<style>
+div {
+  display: flex;
+}
+
+.item {
+  width: 100px;
+  background: red;
+  align-items: center;
+  min-height: 100px;
+}
+
+.item span {
+  min-height: 100%;
+  width: 100%;
+  background: green;
+}
+</style>
+<p>Test passes if you see a green 100px x 100px square, and no red</p>
+<div>
+  <div class="item">
+    <span></span>
+  </div>
+</div>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-003.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-003.html
new file mode 100644
index 0000000..bc5b075
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-003.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: nested flex containers with definite max-height</title>
+<link rel="match" href="flexbox-definite-sizes-001-ref.html">
+<link rel="author" href="mailto:dholbert@mozilla.com" title="Daniel Holbert">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#definite-sizes">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1449326">
+<style>
+body { overflow: hidden }
+
+.outerFlex {
+  display: flex;
+  width: 100px;
+  /* Implicit "align-items:stretch" */
+}
+
+.innerFlex {
+  display: flex;
+  width: 100px;
+  background: red;
+
+  /* This reveals if we miscalculate the height of our flex item: */
+  align-items: flex-end;
+}
+
+.block {
+  width: 100px;
+  max-height: 100%;
+  background-color: green;
+}
+</style>
+<p>Test passes if you see a green 100px x 100px square, and no red</p>
+<div class="outerFlex" style="max-height: 100px">
+  <div class="innerFlex">
+    <div class="block"><div style="height:9999px"></div></div>
+  </div>
+</div>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-004.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-004.html
new file mode 100644
index 0000000..cf54aab
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-definite-sizes-004.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: nested flex containers with definite max-height</title>
+<link rel="match" href="flexbox-definite-sizes-001-ref.html">
+<link rel="author" href="mailto:dholbert@mozilla.com" title="Daniel Holbert">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#definite-sizes">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1449326">
+<style>
+body { overflow: hidden }
+
+.outerFlex {
+  display: flex;
+  width: 100px;
+  /* Implicit "align-items:stretch" */
+}
+
+.innerFlex {
+  display: flex;
+  width: 100px;
+  background: red;
+
+  /* This reveals if we miscalculate the height of our flex item: */
+  align-items: flex-end;
+}
+
+.block {
+  width: 100px;
+  max-height: 100%;
+  background-color: green;
+}
+</style>
+<p>Test passes if you see a green 100px x 100px square, and no red</p>
+<div class="outerFlex">
+  <div class="innerFlex" style="max-height: 100px">
+    <div class="block"><div style="height:9999px"></div></div>
+  </div>
+</div>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003-ref.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003-ref.html
new file mode 100644
index 0000000..63ce9d7
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003-ref.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>CSS Reftest Reference</title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <style>
+  .container {
+    clear: both; /* In this reference case, we use floats instead of
+                    flex items (see below), so the container just
+                    needs to reset the float state for each example. */
+  }
+
+  .item {
+    border: 2px solid teal;
+    float: left; /* Use floated elements as a reference for (hopefully)
+                    max-content sized flex items in testcase. */
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- In testcase, flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- In testcase, flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- In testcase, flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- In testcase, flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- In testcase, flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- In testcase, flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003a.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003a.html
new file mode 100644
index 0000000..83dbae0
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003a.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>
+    CSS Test: Testing that explicit "flex-basis: content" is treated as
+    "max-content" when calculating flex base size
+  </title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#flex-base-size">
+  <link rel="match" href="flexbox-flex-basis-content-003-ref.html">
+  <style>
+  .container {
+    display: flex;
+    /* flex container has an extremely-constrained width (and items will
+       overflow horizontally). This is intentional, as part of stress-testing
+       item sizing. */
+    width: 1px;
+  }
+
+  .item {
+    /* We give all flex items "flex-basis: content".
+       We also give them zero flex-grow, flex-shrink, and min-main-size, so
+       that the flex base size entirely determines the flex item's size. */
+    flex: 0 0 content;
+    min-width: 0;
+    border: 2px solid teal;
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- The idea of this test is to be sure the UA is using the "max-content" size
+     (and not e.g. the "fit-content size") when resolving the flex base size
+     inside each flex container.  To differentiate between max-content and
+     other intrinsic size possibilities (min-content/fit-content), we:
+       - use flex items with a large difference between its min-content size &
+       its max-content size (e.g. wrappable content).
+       - use a very small container (to compress the size, if the UA incorrectly
+       allows the size to be influenced by the container size).
+-->
+
+<!-- Flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- Flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- Flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- Flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- Flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- Flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003b.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003b.html
new file mode 100644
index 0000000..a81403c
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-003b.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>
+    CSS Test: Testing that used "flex-basis: content" is treated as
+    "max-content" when calculating flex base size
+  </title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#flex-base-size">
+  <link rel="match" href="flexbox-flex-basis-content-003-ref.html">
+  <style>
+  .container {
+    display: flex;
+    /* flex container has an extremely-constrained width (and items will
+       overflow horizontally). This is intentional, as part of stress-testing
+       item sizing. */
+    width: 1px;
+  }
+
+  .item {
+    /* We give all flex items a used "flex-basis" of "content"
+       (from "flex-basis:auto" and default "width:auto").
+       We also give them zero flex-grow, flex-shrink, and min-main-size, so
+       that the flex base size entirely determines the flex item's size. */
+    flex: 0 0 auto;
+    min-width: 0;
+    border: 2px solid teal;
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- The idea of this test is to be sure the UA is using the "max-content" size
+     (and not e.g. the "fit-content size") when resolving the flex base size
+     inside each flex container.  To differentiate between max-content and
+     other intrinsic size possibilities (min-content/fit-content), we:
+       - use flex items with a large difference between its min-content size &
+       its max-content size (e.g. wrappable content).
+       - use a very small container (to compress the size, if the UA incorrectly
+       allows the size to be influenced by the container size).
+-->
+
+<!-- Flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- Flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- Flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- Flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- Flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- Flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004-ref.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004-ref.html
new file mode 100644
index 0000000..7da4de7
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004-ref.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>CSS Reftest Reference</title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <style>
+  .container {
+    clear: both; /* In this reference case, we use floats instead of
+                    flex items (see below), so the container just
+                    needs to reset the float state for each example. */
+    height: 50px;
+  }
+
+  .item {
+    border: 2px solid teal;
+    float: left; /* Use floated elements as a reference for (hopefully)
+                    max-content sized flex items in testcase. */
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+    flex-direction: column;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- In testcase, flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- In testcase, flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- In testcase, flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- In testcase, flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- In testcase, flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- In testcase, flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004a.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004a.html
new file mode 100644
index 0000000..65a86b5
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004a.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>
+    CSS Test: Testing that explicit "flex-basis: content" is treated as
+    "max-content" when calculating flex base size
+  </title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#flex-base-size">
+  <link rel="match" href="flexbox-flex-basis-content-004-ref.html">
+  <style>
+  .container {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    /* flex container has an extremely-constrained height (and items will
+       overflow vertically). This is intentional, as part of stress-testing
+       item sizing. We add a large margin-bottom so that overflowing
+       items don't overlap between examples. */
+    height: 1px;
+    margin-bottom: 49px;
+  }
+
+  .item {
+    /* We give all flex items "flex-basis: content".
+       We also give them zero flex-grow, flex-shrink, and min-main-size, so
+       that the flex base size entirely determines the flex item's size. */
+    flex: 0 0 content;
+    min-height: 0;
+    border: 2px solid teal;
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+    flex-direction: column;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- This test exists for symmetry with the previous set of tests
+     (flexbox-flex-basis-content-003*). Those previous tests check how
+     "flex-basis:content" is resolved to a flex base size, in the inline axis,
+     when the container's size is constrained in that axis. This test does the
+     same, but for the *block* axis, using flex-direction:column. As with the
+     previous set of tests, the expectation here is that we should use the
+     item's max-content size as its flex base size. Note that there's a bit
+     less subtlety here because intrinsic sizes (min-content, max-content) are
+     typically all the same in the block axis.
+-->
+
+<!-- Flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- Flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- Flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- Flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- Flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- Flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004b.html b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004b.html
new file mode 100644
index 0000000..a686f1a
--- /dev/null
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/flexbox-flex-basis-content-004b.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>
+    CSS Test: Testing that used "flex-basis: content" is treated as
+    "max-content" when calculating flex base size
+  </title>
+  <meta charset="utf-8">
+  <link rel="author" title="Daniel Holbert" href="mailto:dholbert@mozilla.com">
+  <link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#flex-base-size">
+  <link rel="match" href="flexbox-flex-basis-content-004-ref.html">
+  <style>
+  .container {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    /* flex container has an extremely-constrained height (and items will
+       overflow vertically). This is intentional, as part of stress-testing
+       item sizing. We add a large margin-bottom so that overflowing
+       items don't overlap between examples. */
+    height: 1px;
+    margin-bottom: 49px;
+  }
+
+  .item {
+    /* We give all flex items a used "flex-basis" of "content"
+       (from "flex-basis:auto" and default "width:auto").
+       We also give them zero flex-grow, flex-shrink, and min-main-size, so
+       that the flex base size entirely determines the flex item's size. */
+    flex: 0 0 auto;
+    min-height: 0;
+    border: 2px solid teal;
+  }
+  ib {
+    display: inline-block;
+    background: blue;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  float {
+    float: left;
+    background: fuchsia;
+    border: 1px solid gray;
+    width: 15px;
+    height: 10px;
+  }
+  canvas {
+    background: brown;
+    border: 1px solid gray;
+  }
+  .innerFlex {
+    display: flex;
+    flex-direction: column;
+  }
+  innerItem {
+    background: salmon;
+    border: 1px solid gray;
+    height: 10px;
+    width: 15px;
+    flex: none;
+  }
+  </style>
+</head>
+<body>
+<!-- This test exists for symmetry with the previous set of tests
+     (flexbox-flex-basis-content-003*). Those previous tests check how
+     "flex-basis:content" is resolved to a flex base size, in the inline axis,
+     when the container's size is constrained in that axis. This test does the
+     same, but for the *block* axis, using flex-direction:column. As with the
+     previous set of tests, the expectation here is that we should use the
+     item's max-content size as its flex base size. Note that there's a bit
+     less subtlety here because intrinsic sizes (min-content, max-content) are
+     typically all the same in the block axis.
+-->
+
+<!-- Flex item has several inline-blocks
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item"><ib></ib><ib></ib><ib></ib></div>
+</div>
+
+<!-- Flex item has several floats: -->
+<div class="container">
+  <div class="item">
+    <float></float>
+    <float></float>
+    <float></float>
+  </div>
+</div>
+
+<!-- Flex item has several inline replaced elements:
+     (no spaces, to avoid any text-layout dependency): -->
+<div class="container">
+  <div class="item">
+    <canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas
+    ><canvas width="15" height="10"></canvas>
+  </div>
+</div>
+
+<!-- Flex item *is* a replaced element: -->
+<div class="container">
+  <canvas class="item" width="25" height="10"></canvas>
+</div>
+
+<!-- Flex item is itself a flex container: -->
+<div class="container">
+  <div class="item innerFlex">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+<!-- Flex item is itself a multi-line flex container: -->
+<div class="container">
+  <div class="item innerFlex" style="flex-wrap: wrap">
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+    <innerItem></innerItem>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/reftest.list b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/reftest.list
index 8f3d730..5c38083 100644
--- a/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/reftest.list
+++ b/css/vendor-imports/mozilla/mozilla-central-reftests/flexbox/reftest.list
@@ -103,6 +103,10 @@
 == flexbox-flex-basis-content-001b.html flexbox-flex-basis-content-001-ref.html
 == flexbox-flex-basis-content-002a.html flexbox-flex-basis-content-002-ref.html
 == flexbox-flex-basis-content-002b.html flexbox-flex-basis-content-002-ref.html
+== flexbox-flex-basis-content-003a.html flexbox-flex-basis-content-003-ref.html
+== flexbox-flex-basis-content-003b.html flexbox-flex-basis-content-003-ref.html
+== flexbox-flex-basis-content-004a.html flexbox-flex-basis-content-004-ref.html
+== flexbox-flex-basis-content-004b.html flexbox-flex-basis-content-004-ref.html
 
 # Tests for flex-flow shorthand property
 == flexbox-flex-flow-001.html flexbox-flex-flow-001-ref.html
@@ -130,6 +134,12 @@
 == flexbox-intrinsic-ratio-006.html flexbox-intrinsic-ratio-006-ref.html
 == flexbox-intrinsic-ratio-006v.html flexbox-intrinsic-ratio-006-ref.html
 
+# Test for definite and indefinite sizes.
+== flexbox-definite-sizes-001.html flexbox-definite-sizes-001-ref.html
+== flexbox-definite-sizes-002.html flexbox-definite-sizes-001-ref.html
+== flexbox-definite-sizes-003.html flexbox-definite-sizes-001-ref.html
+== flexbox-definite-sizes-004.html flexbox-definite-sizes-001-ref.html
+
 # Tests for flex items as (pseudo) stacking contexts
 == flexbox-items-as-stacking-contexts-001.xhtml flexbox-items-as-stacking-contexts-001-ref.xhtml
 == flexbox-items-as-stacking-contexts-002.html flexbox-items-as-stacking-contexts-002-ref.html
diff --git a/docs/_writing-tests/file-names.md b/docs/_writing-tests/file-names.md
index 065be4e..797b7d3 100644
--- a/docs/_writing-tests/file-names.md
+++ b/docs/_writing-tests/file-names.md
@@ -52,7 +52,7 @@
 
 `.any`
  : (js files only) Indicates that the file generates tests in which it
-    is run in Window and dedicated worker environments.
+    is [run in multiple scopes][multi-global-tests].
 
 `.tentative`
  : Indicates that a test makes assertions not yet required by any specification,
@@ -63,3 +63,4 @@
 
 
 [server-side substitution]: https://wptserve.readthedocs.io/en/latest/pipes.html#sub
+[multi-global-tests]: {{ site.baseurl }}{% link _writing-tests/testharness.md %}#multi-global-tests
diff --git a/docs/_writing-tests/server-features.md b/docs/_writing-tests/server-features.md
index 495426a..e876e63 100644
--- a/docs/_writing-tests/server-features.md
+++ b/docs/_writing-tests/server-features.md
@@ -14,15 +14,18 @@
 
 ### Tests Involving Multiple Origins
 
-In the test environment, five subdomains are available: `www`, `www1`,
-`www2`, `天気の良い日`, and `élève`; there is also
-`nonexistent-origin` which is guaranteed not to resolve. In addition,
-the HTTP server listens on two ports, and the WebSockets server on
-one. These subdomains and ports must be used for cross-origin
-tests. Tests must not hardcode the hostname of the server that they
-expect to be running on or the port numbers, as these are not
-guaranteed by the test environment. Instead they can get this
-information in one of two ways:
+Our test servers are guaranteed to be accessible through two domains
+and five subdomains under each. The 'main' domain is unnamed; the
+other is called 'alt'. These subdomains are: `www`, `www1`, `www2`,
+`天気の良い日`, and `élève`; there is also `nonexistent-origin` which
+is guaranteed not to resolve. In addition, the HTTP server listens on
+two ports, and the WebSockets server on one. These subdomains and
+ports must be used for cross-origin tests.
+
+Tests must not hardcode the hostname of the server that they expect to
+be running on or the port numbers, as these are not guaranteed by the
+test environment. Instead they can get this information in one of two
+ways:
 
 * From script, using the `location` API.
 
@@ -33,15 +36,19 @@
 through a URL containing `pipe=sub` in the query string
 e.g. `example-test.html?pipe=sub`. The substitution syntax uses `{%
 raw %}{{ }}{% endraw %}` to delimit items for substitution. For
-example to substitute in the host name on which the tests are running,
-one would write: `{% raw %}{{host}}{% endraw %}`.
+example to substitute in the main host name, one would write:
+`{% raw %}{{host}}{% endraw %}`.
 
+To get full domains, including subdomains, there is the `hosts`
+dictionary, where the first dimension is the name of the domain, and
+the second the subdomain. For example, `{% raw %}{{hosts[][www]}}{%
+endraw %}` would give the `www` subdomain under the main (unnamed)
+domain, and `{% raw %}{{hosts[alt][élève]}}{% endraw %}` would give
+the `élève` subdomain under the alt domain.
 
-As well as the host, one can get full domains, including subdomains
-using the `domains` dictionary. For example, `{% raw
-%}{{domains[www]}}{% endraw %}` or `{% raw %}{{domains[élève]}}{%
-endraw %}` would be replaced by the full qualified domain name of the
-respective subdomains.
+For mostly historic reasons, the subdomains of the main domain are
+also available under the `domains` dictionary; this is identical to
+`hosts[]`.
 
 Ports are also available on a per-protocol basis. For example, `{% raw
 %}{{ports[ws][0]}}{% endraw %}` is replaced with the first (and only)
diff --git a/docs/_writing-tests/testharness.md b/docs/_writing-tests/testharness.md
index 96a36ce..e76d8bd 100644
--- a/docs/_writing-tests/testharness.md
+++ b/docs/_writing-tests/testharness.md
@@ -59,13 +59,11 @@
 
 ### Multi-global tests
 
-Tests for features that exist in multiple global scopes can be written
-in a way that they are automatically run in a window scope and a
-worker scope.
+Tests for features that exist in multiple global scopes can be written in a way
+that they are automatically run in several scopes. In this case, the test is a
+JavaScript file with extension `.any.js`, which can use all the usual APIs.
 
-In this case, the test is a JavaScript file with extension `.any.js`.
-The test can then use all the usual APIs, and can be run from the path to the
-JavaScript file with the `.js` replaced by `.worker.html` or `.html`.
+By default, the test runs in a window scope and a dedicated worker scope.
 
 For example, one could write a test for the `Blob` constructor by
 creating a `FileAPI/Blob-constructor.any.js` as follows:
@@ -80,6 +78,30 @@
 This test could then be run from `FileAPI/Blob-constructor.any.worker.html` as well
 as `FileAPI/Blob-constructor.any.html`.
 
+It is possible to customize the set of scopes with a metadata comment, such as
+
+    // META: global=sharedworker
+    //       ==> would run in the default window and dedicated worker scopes,
+    //           as well as the shared worker scope
+    // META: global=!default,serviceworker
+    //       ==> would only run in the service worker scope
+    // META: global=!window
+    //       ==> would run in the default dedicated worker scope, but not the
+    //           window scope
+    // META: global=worker
+    //       ==> would run in the default window scope, as well as in the
+    //           dedicated, shared and service worker scopes
+
+For a test file <code><var>x</var>.any.js</code>, the available scope keywords
+are:
+
+* `window` (default): to be run at <code><var>x</var>.any.html</code>
+* `dedicatedworker` (default): to be run at <code><var>x</var>.any.worker.html</code>
+* `serviceworker`: to be run at <code><var>x</var>.https.any.serviceworker.html</code>
+* `sharedworker`: to be run at <code><var>x</var>.any.sharedworker.html</code>
+* `default`: shorthand for the default scopes
+* `worker`: shorthand for the dedicated, shared and service worker scopes
+
 To check if your test is run from a window or worker you can use the following two methods that will
 be made available by the framework:
 
@@ -99,6 +121,13 @@
 
 Use `// META: timeout=long` at the beginning of the resource.
 
+### Specifying test variants in auto-generated boilerplate tests
+
+Use `// META: variant=url-suffix` at the beginning of the resource. For example,
+
+    // META: variant=
+    // META: variant=?wss
+
 
 [general guidelines]: {{ site.baseurl }}{% link _writing-tests/general-guidelines.md %}
 [testharness-api]: {{ site.baseurl }}{% link _writing-tests/testharness-api.md %}
diff --git a/dom/events/Event-returnValue.html b/dom/events/Event-returnValue.html
index 50c2660..08df2d4 100644
--- a/dom/events/Event-returnValue.html
+++ b/dom/events/Event-returnValue.html
@@ -53,7 +53,7 @@
   assert_true(ev.returnValue, "returnValue");
 }, "initEvent should unset returnValue.");
 test(function() {
-  var ev = new Event("foo");
+  var ev = new Event("foo", {"cancelable": true});
   ev.preventDefault();
   ev.returnValue = true;// no-op
   assert_true(ev.defaultPrevented);
diff --git a/dom/historical.html b/dom/historical.html
index 388366c..b45bebf 100644
--- a/dom/historical.html
+++ b/dom/historical.html
@@ -12,12 +12,14 @@
 }
 var nukedInterfaces = [
   "DOMConfiguration",
+  "DOMCursor",
   "DOMError",
   "DOMErrorHandler",
   "DOMImplementationList",
   "DOMImplementationSource",
   "DOMLocator",
   "DOMObject",
+  "DOMRequest",
   "DOMSettableTokenList",
   "DOMUserData",
   "Entity",
diff --git a/dom/lists/DOMTokenList-coverage-for-attributes.html b/dom/lists/DOMTokenList-coverage-for-attributes.html
index 034e31c..e5f060b 100644
--- a/dom/lists/DOMTokenList-coverage-for-attributes.html
+++ b/dom/lists/DOMTokenList-coverage-for-attributes.html
@@ -10,9 +10,10 @@
 var pairs = [
   // Defined in DOM
   {attr: "classList", sup: ["anyElement"]},
+  // Defined in HTML except for a which is also SVG
+  {attr: "relList", sup: ["a", "area", "link"]},
   // Defined in HTML
   {attr: "htmlFor", sup: ["output"]},
-  {attr: "relList", sup: ["a", "area", "link"]},
   {attr: "sandbox", sup: ["iframe"]},
   {attr: "sizes", sup: ["link"]}
 ];
@@ -26,7 +27,11 @@
 
 var elements = ["a", "area", "link", "iframe", "output", "td", "th"];
 function testAttr(pair, new_el){
-  return (pair.attr === "classList" || (new_el.namespaceURI === "http://www.w3.org/1999/xhtml" && pair.sup.indexOf(new_el.localName) != -1));
+  return (pair.attr === "classList" ||
+          (pair.attr === "relList" && new_el.localName === "a" &&
+           new_el.namespaceURI === "http://www.w3.org/2000/svg") ||
+          (new_el.namespaceURI === "http://www.w3.org/1999/xhtml" &&
+           pair.sup.indexOf(new_el.localName) != -1));
 }
 
 pairs.forEach(function(pair) {
diff --git a/dom/nodes/Document-constructor.html b/dom/nodes/Document-constructor.html
index 2f26982..ceb4192 100644
--- a/dom/nodes/Document-constructor.html
+++ b/dom/nodes/Document-constructor.html
@@ -36,6 +36,7 @@
   assert_equals(doc.contentType, "application/xml");
   assert_equals(doc.origin, document.origin);
   assert_equals(doc.createElement("DIV").localName, "DIV");
+  assert_equals(doc.createElement("a").constructor, Element);
 }, "new Document(): metadata")
 
 test(function() {
@@ -47,7 +48,8 @@
 
 test(function() {
   var doc = new Document();
-  var a = doc.createElement("a");
+  var a = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+  assert_equals(a.constructor, HTMLAnchorElement);
   // In UTF-8: 0xC3 0xA4
   a.href = "http://example.org/?\u00E4";
   assert_equals(a.href, "http://example.org/?%C3%A4");
diff --git a/dom/nodes/Node-cloneNode.html b/dom/nodes/Node-cloneNode.html
index 6c86630..08d620b 100644
--- a/dom/nodes/Node-cloneNode.html
+++ b/dom/nodes/Node-cloneNode.html
@@ -240,7 +240,6 @@
     assert_equals(doc.contentType, copy.contentType, "contentType equality");
     assert_equals(doc.URL, "about:blank", "URL value")
     assert_equals(doc.URL, copy.URL, "URL equality");
-    assert_equals(doc.origin, "null", "origin value")
     assert_equals(doc.origin, copy.origin, "origin equality");
     assert_equals(doc.compatMode, "CSS1Compat", "compatMode value");
     assert_equals(doc.compatMode, copy.compatMode, "compatMode equality");
@@ -286,4 +285,17 @@
     assert_equals(copy.childNodes.length, 0,
                   "copy.childNodes.length with non-deep copy");
 }, "node with children");
+
+test(() => {
+  const proto = Object.create(HTMLElement.prototype),
+        node = document.createElement("hi");
+  Object.setPrototypeOf(node, proto);
+  assert_true(proto.isPrototypeOf(node));
+  const clone = node.cloneNode();
+  assert_false(proto.isPrototypeOf(clone));
+  assert_true(HTMLUnknownElement.prototype.isPrototypeOf(clone));
+  const deepClone = node.cloneNode(true);
+  assert_false(proto.isPrototypeOf(deepClone));
+  assert_true(HTMLUnknownElement.prototype.isPrototypeOf(deepClone));
+}, "Node with custom prototype")
 </script>
diff --git a/encoding/idlharness.any.js b/encoding/idlharness.any.js
new file mode 100644
index 0000000..25ec97a
--- /dev/null
+++ b/encoding/idlharness.any.js
@@ -0,0 +1,14 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+promise_test(async() => {
+  const text = await (await fetch('/interfaces/encoding.idl')).text();
+  const idl_array = new IdlArray();
+  idl_array.add_idls(text);
+  idl_array.add_objects({
+    TextEncoder: ['new TextEncoder()'],
+    TextDecoder: ['new TextDecoder()']
+  });
+  idl_array.test();
+}, 'Encoding Standard IDL');
diff --git a/encoding/idlharness.html b/encoding/idlharness.html
deleted file mode 100644
index 03cec92..0000000
--- a/encoding/idlharness.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>idlharness test: Encoding Living Standard API</title>
-<link rel="author" title="Joshua Bell" href="mailto:jsbell@google.com" />
-<link rel="help" href="https://encoding.spec.whatwg.org/#api"/>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/WebIDLParser.js"></script>
-<script src="/resources/idlharness.js"></script>
-
-<h1>idlharness test</h1>
-<p>This test validates the WebIDL included in the Encoding Living Standard.</p>
-
-<script>
-promise_test(async() => {
-  const text = await (await fetch('/interfaces/encoding.idl')).text();
-  const idl_array = new IdlArray();
-  idl_array.add_idls(text);
-  idl_array.add_objects({
-    TextEncoder: ['new TextEncoder()'],
-    TextDecoder: ['new TextDecoder()']
-  });
-  idl_array.test();
-}, 'Test driver');
-</script>
diff --git a/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-form-csiso2022jp.html b/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-form-csiso2022jp.html
index 8089b83..213791a 100644
--- a/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-form-csiso2022jp.html
+++ b/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-form-csiso2022jp.html
@@ -7,6 +7,7 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="jis0208_index.js"></script>
+<script src="iso2022jp-encoder.js"></script>
 <link rel="author" title="Richard Ishida" href="mailto:ishida@w3.org">
 <link rel="help" href="https://encoding.spec.whatwg.org/#names-and-labels">
 <meta name="assert" content="The browser produces the same encoding behavior for a document labeled 'csiso2022jp' as for a document labeled 'iso-2022-jp'.">
diff --git a/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-href-ascii.html b/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-href-ascii.html
deleted file mode 100644
index d5a747b..0000000
--- a/encoding/legacy-mb-japanese/iso-2022-jp/iso2022jp-encode-href-ascii.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="iso-2022-jp"> <!-- test breaks if the server overrides this -->
-<title>ISO 2022-JP encoding (href)</title>
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="jis0208_index.js"></script>
-<script src="iso2022jp-encoder.js"></script>
-<link rel="author" title="Richard Ishida" href="mailto:ishida@w3.org">
-<link rel="help" href="https://encoding.spec.whatwg.org/#iso-2022-jp">
-<meta name="assert" content="The browser produces the expected byte sequences for all characters in the iso-2022-jp encoding after 0x9F when writing characters to an href value, using the encoder steps in the specification.">
-<script>
-function XnormalizeStr(str) {
-    str = encodeURIComponent(str);
-    var ptr = 0;
-    var out = "";
-    for (c = 0; c < str.length; c++) {
-        if (str.charAt(ptr) == "%") {
-            out += String.fromCodePoint(
-                parseInt(str.charAt(ptr + 1) + str.charAt(ptr + 2), 16)
-            );
-            ptr += 2;
-        } else out += str.charAt(ptr);
-        ptr++;
-    }
-}
-
-function encode(input, expected, desc) {
-    // tests whether a Unicode character is converted to an equivalent byte sequence by href
-    // input: a Unicode character
-    // expected: expected byte sequence
-    // desc: what's being tested
-    test(function() {
-        var a = document.createElement("a"); // <a> uses document encoding for URL's query
-        a.href = "https://example.com/?" + input;
-        var result = a.search.substr(1); // remove leading "?"
-        assert_equals(normalizeStr(result), normalizeStr(expected));
-    }, desc);
-}
-
-// create a simple list of just those code points for which there is an encoding possible
-codepoints = [];
-for (var i = 0x00; i < 0xfe; i++) {
-    result = iso2022jpEncoder(String.fromCodePoint(i));
-    if (result) {
-        var item = {};
-        codepoints.push(item);
-        item.cp = i;
-        item.expected = "%" + result.replace(/ /g, "%");
-        item.expected = item.expected.replace(/%1B%28%42$/, "");
-    }
-}
-
-// run the tests
-encode(String.fromCodePoint(0x65), "e;", "%65");
-encode(String.fromCodePoint(0x1b), "d;", "%1B");
-
-//for (var x=0;x<codepoints.length;x++) {
-//  encode(String.fromCodePoint(codepoints[x].cp), codepoints[x].expected, "U+"+codepoints[x].cp.toString(16).toUpperCase()+' '+String.fromCodePoint(codepoints[x].cp)+" "+codepoints[x].expected)
-//  }
-
-// NOTES
-// this test relies on support for String.fromCodePoint, which appears to be supported by major desktop browsers
-// the test excludes ASCII characters
-</script>
-</head>
-<body>
-<div id="log"></div>
-</body>
-</html>
diff --git a/encoding/legacy-mb-korean/euc-kr/euckr-encode-form-errors-hangul.html b/encoding/legacy-mb-korean/euc-kr/euckr-encode-form-errors-hangul.html
deleted file mode 100644
index 1a79c6f..0000000
--- a/encoding/legacy-mb-korean/euc-kr/euckr-encode-form-errors-hangul.html
+++ /dev/null
@@ -1,181 +0,0 @@
-<!DOCTYPE html>
-<html>   <!-- DOESN'T WORK, NOT SURE WHY - THERE ARE 11 CHARACTERS PRODUCED, BUT ALL ARE UNASSIGNED CODE POINTS, SO SAFE TO IGNORE FOR NOW -->
-<head>
-<title>EUC-KR encoding errors (form, hangul)</title>
-<meta charset="euc-kr"> <!-- test breaks if the server overrides this -->
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="euckr_index.js"></script>
-<link rel="author" title="Richard Ishida" href="mailto:ishida@w3.org">
-<link rel="help" href="https://encoding.spec.whatwg.org/#euc-kr">
-<meta name="assert" content="The browser produces percent-escaped character references when encoding bytes for a URL produced by a form when encoding hangul characters that are not in the euc-kr encoding.">
-<style>
- iframe { display:none }
- form { display:none }
-</style>
-</head>
-<body>
-<div id="log"></div>
-<script>
-var tests = [];
-var cplist = [];
-var numTests = null;
-var numFrames = 2;
-var chunkSize = 400;
-var numChunks = null;
-var frames = null;
-var frames = null;
-var forms = null;
-var seperator = ",";
-var encodedSeperator = encodeURIComponent(",");
-var currentChunkIndex = 0;
-
-function getByteSequence(cp) {
-  // uses the Encoding spec algorithm to derive a sequence of bytes for a character that can be encoded
-  // the result is either a percent-encoded value or null (if the character can't be encoded)
-  // cp: integer, a code point number
-  var cps = [cp];
-  var out = "";
-
-  while (cps.length > 0) {
-    cp = cps.shift();
-    if (cp >= 0x00 && cp <= 0x7f) {
-      // ASCII
-      out += "%" + cp.toString(16);
-      continue;
-    }
-    var ptr = indexcodepoints[cp];
-    if (ptr == null) {
-      return null;
-    }
-    var lead = Math.floor(ptr / 190) + 0x81;
-    var trail = ptr % 190 + 0x41;
-    out +=
-      "%" +
-      lead.toString(16).toUpperCase() +
-      "%" +
-      trail.toString(16).toUpperCase();
-  }
-  return out;
-}
-
-// set up a sparse array of all unicode codepoints listed in the index
-// this will be used for lookup in getByteSequence
-var indexcodepoints = []; // index is unicode cp, value is pointer
-for (p = 0; p < euckr.length; p++) {
-  if (euckr[p] != null && indexcodepoints[euckr[p]] == null) {
-    indexcodepoints[euckr[p]] = p;
-  }
-}
-
-setup(function() {
-  // set up a simple array of all unicode codepoints that are not encoded
-  var codepoints = [];
-
-  for (i = 0xac00; i < 0xd7af; i++) {
-    result = getByteSequence(i);
-    if (!result) {
-      var item = {};
-      codepoints.push(item);
-      item.cp = i;
-      item.expected = "%26%23" + item.cp + "%3B";
-      item.desc = "hangul ";
-    }
-  }
-
-  // convert the information into a simple array of objects that can be easily traversed
-  var currentChunk = [];
-  var currentTests = [];
-  cplist = [currentChunk];
-  tests = [currentTests];
-  for (i = 161; i < codepoints.length; i++) {
-    if (currentChunk.length == chunkSize) {
-      currentChunk = [];
-      cplist.push(currentChunk);
-      currentTests = [];
-      tests.push(currentTests);
-    }
-    var item = {};
-    currentChunk.push(item);
-    item.cp = codepoints[i].cp;
-    item.expected = codepoints[i].expected;
-    item.desc = codepoints[i].desc;
-    currentTests.push(
-      async_test(
-        item.desc +
-          " U+" +
-          item.cp.toString(16).toUpperCase() +
-          " " +
-          String.fromCodePoint(item.cp) +
-          item.expected
-      )
-    );
-  }
-
-  numChunks = cplist.length;
-
-  for (var i = 0; i < numFrames; i++) {
-    var frame = document.createElement("iframe");
-    frame.id = frame.name = "frame-" + i;
-    document.body.appendChild(frame);
-    var form = document.createElement("form");
-    form.id = "form-" + i;
-    form.method = "GET";
-    form.action = "/common/blank.html";
-    form.acceptCharset = "euc-kr";
-    form.target = frame.id;
-    var input = document.createElement("input");
-    input.id = input.name = "input-" + i;
-    form.appendChild(input);
-    document.body.appendChild(form);
-  }
-
-  addEventListener("load", function() {
-    frames = Array.prototype.slice.call(
-      document.getElementsByTagName("iframe")
-    );
-    forms = Array.prototype.slice.call(document.getElementsByTagName("form"));
-    inputs = Array.prototype.slice.call(document.getElementsByTagName("input"));
-    for (var i = 0; i < Math.min(numFrames, numChunks); i++) {
-      runNext(i);
-    }
-  });
-});
-
-function runNext(id) {
-  var i = currentChunkIndex;
-  currentChunkIndex += 1;
-
-  var iframe = frames[id];
-  var form = forms[id];
-  var input = inputs[id];
-
-  input.value = cplist[i]
-    .map(function(x) {
-      return String.fromCodePoint(x.cp);
-    })
-    .join(seperator);
-  form.submit();
-
-  iframe.onload = function() {
-    var url = iframe.contentWindow.location;
-    var query = url.search;
-    var result_string = query.substr(query.indexOf("=") + 1);
-    var results = result_string.split(encodedSeperator);
-
-    for (var j = 0; j < cplist[i].length; j++) {
-      var t = tests[i][j];
-      t.step(function() {
-        assert_equals(results[j], cplist[i][j].expected); // HERE'S THE TEST
-      });
-      t.done();
-    }
-    if (currentChunkIndex < numChunks) {
-      runNext(id);
-    }
-  };
-}
-</script>
-</body>
-</html>
diff --git a/encoding/legacy-mb-tchinese/big5/big5-enc-ascii.html b/encoding/legacy-mb-tchinese/big5/big5-enc-ascii.html
index 0065ab5..9195920 100644
--- a/encoding/legacy-mb-tchinese/big5/big5-enc-ascii.html
+++ b/encoding/legacy-mb-tchinese/big5/big5-enc-ascii.html
@@ -33,6 +33,10 @@
 
 // test ASCII - test separately for chars that aren't escaped
 for (var a = 0; a < 0x7f; a++) {
+  // The first 3 are stripped from URLs and the last is # which introduces a new URL segment
+  if (a === 0x09 || a === 0x0a || a === 0x0d || a === 0x23) {
+    continue;
+  }
   hex = a.toString(16).toUpperCase();
   while (hex.length < 2) {
     hex = "0" + hex;
diff --git a/encrypted-media/scripts/requestmediakeysystemaccess.js b/encrypted-media/scripts/requestmediakeysystemaccess.js
index a6b0a96..edfcbfc 100644
--- a/encrypted-media/scripts/requestmediakeysystemaccess.js
+++ b/encrypted-media/scripts/requestmediakeysystemaccess.js
@@ -91,10 +91,12 @@
         initDataTypes: [config.initDataType],
         audioCapabilities: [{contentType: config.audioType}],
         videoCapabilities: [{contentType: config.videoType}],
+        label: 'abcd',
     }], {
         initDataTypes: [config.initDataType],
         audioCapabilities: [{contentType: config.audioType}],
         videoCapabilities: [{contentType: config.videoType}],
+        label: 'abcd',
     }, 'Basic supported configuration');
 
     expect_config(config.keysystem, [{
diff --git a/feature-policy/experimental-features/resources/vertical-scroll-scrollintoview.html b/feature-policy/experimental-features/resources/vertical-scroll-scrollintoview.html
new file mode 100644
index 0000000..7bed27c
--- /dev/null
+++ b/feature-policy/experimental-features/resources/vertical-scroll-scrollintoview.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<style>
+html, body, #container {
+  width: 100%;
+  height: 100%;
+}
+
+#spacer {
+  width: 200%;
+  height: 200%;
+}
+</style>
+<div id="container">
+  <div id="spacer"></div>
+  <button>Element To Scroll</button>
+</div>
+<script>
+  window.addEventListener('message', onMessageReceived);
+
+  function scrollingElementBounds() {
+    var rect = document.querySelector("button").getBoundingClientRect();
+    return {
+        x: rect.x, y: rect.y, width: rect.width, height: rect.height
+      };
+  }
+
+  function onMessageReceived(e) {
+    if (!e.data || !e.data.type)
+      return;
+    switch(e.data.type) {
+      case "scroll":
+        document.querySelector("button").scrollIntoView({behavior: "instant"});
+        ackMessage({bounds: scrollingElementBounds()}, e.source);
+      break;
+
+      case "scrolling-element-bounds":
+        ackMessage({bounds: scrollingElementBounds()}, e.source);
+      break;
+    }
+  }
+
+  function ackMessage(msg, source) {
+    source.postMessage(msg, "*");
+  }
+</script>
diff --git a/feature-policy/experimental-features/resources/vertical-scroll.js b/feature-policy/experimental-features/resources/vertical-scroll.js
new file mode 100644
index 0000000..bbae658
--- /dev/null
+++ b/feature-policy/experimental-features/resources/vertical-scroll.js
@@ -0,0 +1,55 @@
+const url_base = "/feature-policy/experimental-features/resources/";
+window.messageResponseCallback = null;
+
+function rectMaxY(rect) {
+  return rect.height + rect.y;
+}
+
+function rectMaxX(rect) {
+  return rect.width + rect.x;
+}
+
+function isEmptyRect(rect) {
+  return !rect.width || !rect.height;
+}
+
+// Returns true if the given rectangles intersect.
+function rects_intersect(rect1, rect2) {
+  if (isEmptyRect(rect1) || isEmptyRect(rect2))
+    return false;
+  return rect1.x < rectMaxX(rect2) &&
+         rect2.x < rectMaxX(rect1) &&
+         rect1.y < rectMaxY(rect2) &&
+         rect2.y < rectMaxY(rect1);
+}
+
+// Returns a promise which is resolved when the <iframe> is navigated to |url|
+// and "load" handler has been called.
+function loadUrlInIframe(iframe, url) {
+  return new Promise((resolve) => {
+    iframe.addEventListener("load", resolve);
+    iframe.src = url;
+  });
+}
+
+// Posts |message| to |target| and resolves the promise with the response coming
+// back from |target|.
+function sendMessageAndGetResponse(target, message) {
+  return new Promise((resolve) => {
+    window.messageResponseCallback = resolve;
+    target.postMessage(message, "*");
+  });
+}
+
+function rectToString(rect) {
+  return `Location: (${rect.x}, ${rect.y}) Size: (${rect.width}, ${rect.height})`;
+}
+
+function onMessage(e) {
+  if (window.messageResponseCallback) {
+    window.messageResponseCallback(e.data);
+    window.messageResponseCallback = null;
+  }
+}
+
+window.addEventListener("message", onMessage);
diff --git a/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html b/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html
new file mode 100644
index 0000000..e9c7cba
--- /dev/null
+++ b/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/feature-policy/experimental-features/resources/vertical-scroll.js"></script>
+<style>
+html, body {
+  height: 100%;
+  width: 100%;
+}
+
+iframe {
+  width: 95%;
+  height: 95%;
+  overflow: scroll;
+  margin-top: 200%;
+}
+
+.spacer {
+  width: 100%;
+  height: 100%;
+  margin-top: 100%;
+  margin-bottom: 100%;
+}
+
+</style>
+<p> An &lt;iframe&gt; further below which is not allowed to block scroll.</p>
+<div class="spacer"></div>
+<iframe></iframe>
+<p> Making sure there is room for vertical scroll </p>
+<script>
+  "use strict";
+
+  let url = url_base + "vertical-scroll-scrollintoview.html";
+  let iframeElement = document.querySelector("iframe");
+
+  function iframeBounds() {
+    return iframeElement.getBoundingClientRect();
+  }
+
+  // Enabled 'vertical-scroll': scrollIntoView should work in all frames.
+  promise_test(async() => {
+    window.scrollTo(0, 0);
+    await loadUrlInIframe(iframeElement, url);
+
+    await sendMessageAndGetResponse(
+      iframeElement.contentWindow,
+      {type: "scrolling-element-bounds"}).then((response) => {
+        let iframeBoundsAtOrigin = {
+          x: 0,
+          y: 0,
+          width: iframeBounds().width,
+          height: iframeBounds().height};
+          let scrollingElementBounds = response.bounds;
+          assert_false(
+            rects_intersect(iframeBoundsAtOrigin, scrollingElementBounds),
+            "Scrolling element should not be visible in <iframe>." +
+            `Scrolling element's bounds is: ${rectToString(response.bounds)}  ` +
+            "but <iframe>'s size is:" +
+            `${iframeBounds().width}x${iframeBounds().height}.`);
+      });
+
+    // Scroll the scrolling element inside the <iframe>.
+    await sendMessageAndGetResponse(iframeElement.contentWindow,
+                                   {type: "scroll"});
+    // The page should have scrolled vertically.
+      assert_greater_than(window.scrollY,
+                          0,
+                          "Main frame must scroll vertically.");
+    }, "Calling 'scrollIntoView()' inside a <iframe> will propagate up by" +
+       " default('vertical-scroll' enabled).");
+
+  // Disabled 'vertical-scroll': The scope of scrollIntoView is within the set
+  // of disabled frames (does not propagate to main frame).
+  promise_test(async() => {
+    window.scrollTo(0, 0);
+    iframeElement.allow = "vertical-scroll 'none';";
+    await loadUrlInIframe(iframeElement, url);
+
+    await sendMessageAndGetResponse(
+      iframeElement.contentWindow,
+      {type: "scrolling-element-bounds"}).then((response) => {
+      let iframeBoundsAtOrigin = {
+        x: 0,
+        y: 0,
+        width: iframeBounds().width,
+        height: iframeBounds().height};
+      let scrollingElementBounds = response.bounds;
+      assert_false(rects_intersect(iframeBoundsAtOrigin, scrollingElementBounds),
+            "Scrolling element should not be visible in <iframe>." +
+            `Scrolling element's bounds is: ${rectToString(response.bounds)}` +
+            "but <iframe>'s size is:" +
+            `${iframeBounds().width}x${iframeBounds().height}.`);
+      });
+
+    // Scroll scrolling element inside the <iframe>.
+    await sendMessageAndGetResponse(iframeElement.contentWindow,
+      {type: "scroll"}).then((response) => {
+      // Make sure the nested <iframe> is visible.
+      let scrollingElementBounds = response.bounds;
+      let iframeBoundsAtOrigin = {
+          x: 0,
+          y: 0,
+          width: iframeBounds().width,
+          height: iframeBounds().height};
+      // The scrolling element should be visible inside <iframe>.
+      assert_true(rects_intersect(iframeBoundsAtOrigin, scrollingElementBounds),
+          "Scrolling element should be visible in <iframe>." +
+          `Scrolling element's bounds is: ${rectToString(response.bounds)}` +
+          "but <iframe>'s size is:" +
+          `${iframeBounds().width}, ${iframeBounds().height}.`);
+      // The page however should not have scrolled.
+      assert_equals(window.scrollY, 0, "Main frame must not scroll vertically.");
+    });
+    }, "Calling 'scrollIntoView()' inside a <iframe> with" +
+       " 'vertical-scroll none;'will not propagate upwards.");
+</script>
diff --git a/feature-policy/feature-policy-frame-policy-allowed-for-self.https.sub.html b/feature-policy/feature-policy-frame-policy-allowed-for-self.https.sub.html
index d757d4c..d71a09f 100644
--- a/feature-policy/feature-policy-frame-policy-allowed-for-self.https.sub.html
+++ b/feature-policy/feature-policy-frame-policy-allowed-for-self.https.sub.html
@@ -28,6 +28,24 @@
     test_frame_policy('fullscreen', cross_origin_src, false);
   }, 'Test frame policy on cross origin iframe inherit from header policy.');
 
+  // Test that frame policy can be used for sandboxed frames
+  test(function() {
+    test_frame_policy(
+      'fullscreen', same_origin_src, false, undefined, false, true);
+    }, 'Test frame policy on sandboxed iframe with no allow attribute.');
+  test(function() {
+    test_frame_policy(
+      'fullscreen', same_origin_src, true, 'fullscreen', false, true);
+    }, 'Test frame policy on sandboxed iframe with allow="fullscreen".');
+  test(function() {
+    test_frame_policy(
+      'fullscreen', same_origin_src, true, 'fullscreen \'src\'', false, true);
+    }, 'Test frame policy on sandboxed iframe with allow="fullscreen \'src\'".');
+  test(function() {
+    test_frame_policy(
+      'fullscreen', cross_origin_src, false, 'fullscreen ' + cross_origin, false, true);
+    }, 'Test frame policy on sandboxed iframe with allow="fullscreen ' + cross_origin + '".');
+
   // Test frame policy with allow attribute set to be one of the policies above.
   for (var i = 0; i < policies.length; i++) {
     test(function() {
diff --git a/feature-policy/resources/featurepolicy.js b/feature-policy/resources/featurepolicy.js
index be8629d..bf7693f 100644
--- a/feature-policy/resources/featurepolicy.js
+++ b/feature-policy/resources/featurepolicy.js
@@ -393,8 +393,10 @@
 //     test_expect: boolean value of whether the feature should be allowed.
 //     allow: optional, the allow attribute (container policy) of the iframe.
 //     allowfullscreen: optional, boolean value of allowfullscreen attribute.
+//     sandbox: optional boolean. If true, the frame will be sandboxed (with
+//         allow-scripts, so that tests can run in it.)
 function test_frame_policy(
-    feature, src, test_expect, allow, allowfullscreen) {
+    feature, src, test_expect, allow, allowfullscreen, sandbox) {
   let frame = document.createElement('iframe');
   document.body.appendChild(frame);
   // frame_policy should be dynamically updated as allow and allowfullscreen is
@@ -406,6 +408,9 @@
   if (!!allowfullscreen) {
     frame.setAttribute('allowfullscreen', true);
   }
+  if (!!sandbox) {
+    frame.setAttribute('sandbox', 'allow-scripts');
+  }
   frame.src = src;
   if (test_expect) {
     assert_true(frame_policy.allowedFeatures().includes(feature));
diff --git a/fetch/api/abort/general-serviceworker.https.html b/fetch/api/abort/general-serviceworker.https.html
deleted file mode 100644
index 74de287..0000000
--- a/fetch/api/abort/general-serviceworker.https.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>General fetch abort tests in a service worker</title>
-  <script src="/resources/testharness.js"></script>
-  <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
-<script>
-  (async function() {
-    const scope = 'does/not/exist';
-
-    let reg = await navigator.serviceWorker.getRegistration(scope);
-    if (reg) await reg.unregister();
-
-    reg = await navigator.serviceWorker.register('general.any.worker.js', {scope});
-
-    fetch_tests_from_worker(reg.installing);
-  })();
-</script>
-</body>
-</html>
diff --git a/fetch/api/abort/general-sharedworker.html b/fetch/api/abort/general-sharedworker.html
deleted file mode 100644
index 9378e16..0000000
--- a/fetch/api/abort/general-sharedworker.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>General fetch abort tests - shared worker</title>
-  <script src="/resources/testharness.js"></script>
-  <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
-<script>
-  fetch_tests_from_worker(new SharedWorker("general.any.worker.js"));
-</script>
-</body>
-</html>
diff --git a/fetch/api/abort/general.any.js b/fetch/api/abort/general.any.js
index 91361cb..4bc404d 100644
--- a/fetch/api/abort/general.any.js
+++ b/fetch/api/abort/general.any.js
@@ -1,3 +1,4 @@
+// META: global=window,worker
 // META: script=/common/utils.js
 // META: script=../request/request-error.js
 
diff --git a/fetch/api/basic/integrity-sharedworker.html b/fetch/api/basic/integrity-sharedworker.html
deleted file mode 100644
index fa90a60..0000000
--- a/fetch/api/basic/integrity-sharedworker.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in sharedworker: integrity handling</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new SharedWorker("integrity.js?pipe=sub"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/basic/integrity-worker.html b/fetch/api/basic/integrity-worker.html
deleted file mode 100644
index 9240bc6..0000000
--- a/fetch/api/basic/integrity-worker.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: integrity handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("integrity.js?pipe=sub"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/integrity.html b/fetch/api/basic/integrity.html
deleted file mode 100644
index 150c9b7..0000000
--- a/fetch/api/basic/integrity.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: integrity handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="integrity.js?pipe=sub"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/integrity.js b/fetch/api/basic/integrity.js
deleted file mode 100644
index fb3f252..0000000
--- a/fetch/api/basic/integrity.js
+++ /dev/null
@@ -1,79 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function integrity(desc, url, integrity, initRequestMode, shouldPass) {
-  var fetchRequestInit = {'integrity': integrity}
-  if (!!initRequestMode && initRequestMode !== "") {
-    fetchRequestInit.mode = initRequestMode;
-  }
-
-  if (shouldPass) {
-    promise_test(function(test) {
-      return fetch(url, fetchRequestInit).then(function(resp) {
-        if (initRequestMode !== "no-cors") {
-          assert_equals(resp.status, 200, "Response's status is 200");
-        } else {
-          assert_equals(resp.status, 0, "Opaque response's status is 0");
-          assert_equals(resp.type, "opaque");
-        }
-      });
-    }, desc);
-  } else {
-    promise_test(function(test) {
-      return promise_rejects(test, new TypeError(), fetch(url, fetchRequestInit));
-    }, desc);
-  }
-}
-
-const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk=";
-const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL";
-const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
-const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I=";
-const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg==";
-
-const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt";
-const url = path;
-const corsUrl =
-  `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`;
-const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}`
-
-integrity("Empty string integrity", url, "", /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("Invalid integrity", url, invalidSha256,
-          /* initRequestMode */ undefined, /* shouldPass */  false);
-integrity("Multiple integrities: valid stronger than invalid", url,
-          invalidSha256 + " " + topSha384, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("Multiple integrities: invalid stronger than valid",
-          url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined,
-          /* shouldPass */ false);
-integrity("Multiple integrities: invalid as strong as valid", url,
-          invalidSha512 + " " + topSha512, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("Multiple integrities: both are valid", url,
-          topSha384 + " " + topSha512, /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("Multiple integrities: both are invalid", url,
-          invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined,
-          /* shouldPass */ false);
-integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined,
-          /* shouldPass */ true);
-integrity("CORS SHA-512 integrity", corsUrl, topSha512,
-          /* initRequestMode */ undefined, /* shouldPass */ true);
-integrity("CORS invalid integrity", corsUrl, invalidSha512,
-          /* initRequestMode */ undefined, /* shouldPass */ false);
-
-integrity("Empty string integrity for opaque response", corsUrl2, "",
-          /* initRequestMode */ "no-cors", /* shouldPass */ true);
-integrity("SHA-* integrity for opaque response", corsUrl2, topSha512,
-          /* initRequestMode */ "no-cors", /* shouldPass */ false);
-
-done();
diff --git a/fetch/api/basic/integrity.sub.any.js b/fetch/api/basic/integrity.sub.any.js
new file mode 100644
index 0000000..d487c37
--- /dev/null
+++ b/fetch/api/basic/integrity.sub.any.js
@@ -0,0 +1,77 @@
+// META: global=sharedworker
+// META: script=../resources/utils.js
+
+function integrity(desc, url, integrity, initRequestMode, shouldPass) {
+  var fetchRequestInit = {'integrity': integrity}
+  if (!!initRequestMode && initRequestMode !== "") {
+    fetchRequestInit.mode = initRequestMode;
+  }
+
+  if (shouldPass) {
+    promise_test(function(test) {
+      return fetch(url, fetchRequestInit).then(function(resp) {
+        if (initRequestMode !== "no-cors") {
+          assert_equals(resp.status, 200, "Response's status is 200");
+        } else {
+          assert_equals(resp.status, 0, "Opaque response's status is 0");
+          assert_equals(resp.type, "opaque");
+        }
+      });
+    }, desc);
+  } else {
+    promise_test(function(test) {
+      return promise_rejects(test, new TypeError(), fetch(url, fetchRequestInit));
+    }, desc);
+  }
+}
+
+const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk=";
+const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL";
+const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I=";
+const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg==";
+
+const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+const url = path;
+const corsUrl =
+  `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`;
+const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}`
+
+integrity("Empty string integrity", url, "", /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("Invalid integrity", url, invalidSha256,
+          /* initRequestMode */ undefined, /* shouldPass */  false);
+integrity("Multiple integrities: valid stronger than invalid", url,
+          invalidSha256 + " " + topSha384, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("Multiple integrities: invalid stronger than valid",
+          url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined,
+          /* shouldPass */ false);
+integrity("Multiple integrities: invalid as strong as valid", url,
+          invalidSha512 + " " + topSha512, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("Multiple integrities: both are valid", url,
+          topSha384 + " " + topSha512, /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("Multiple integrities: both are invalid", url,
+          invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined,
+          /* shouldPass */ false);
+integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined,
+          /* shouldPass */ true);
+integrity("CORS SHA-512 integrity", corsUrl, topSha512,
+          /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("CORS invalid integrity", corsUrl, invalidSha512,
+          /* initRequestMode */ undefined, /* shouldPass */ false);
+
+integrity("Empty string integrity for opaque response", corsUrl2, "",
+          /* initRequestMode */ "no-cors", /* shouldPass */ true);
+integrity("SHA-* integrity for opaque response", corsUrl2, topSha512,
+          /* initRequestMode */ "no-cors", /* shouldPass */ false);
+
+done();
diff --git a/fetch/api/basic/mediasource.window.js b/fetch/api/basic/mediasource.window.js
new file mode 100644
index 0000000..ff58a52
--- /dev/null
+++ b/fetch/api/basic/mediasource.window.js
@@ -0,0 +1,5 @@
+promise_test(t => {
+  const mediaSource = new MediaSource(),
+        mediaSourceURL = URL.createObjectURL(mediaSource);
+  return promise_rejects(t, new TypeError(), fetch(mediaSourceURL));
+}, "Cannot fetch blob: URL from a MediaSource");
diff --git a/fetch/api/basic/mode-no-cors-worker.html b/fetch/api/basic/mode-no-cors-worker.html
deleted file mode 100644
index 87376a1..0000000
--- a/fetch/api/basic/mode-no-cors-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: no-cors mode and opaque filtering</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("mode-no-cors.js?pipe=sub"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/mode-no-cors.html b/fetch/api/basic/mode-no-cors.html
deleted file mode 100644
index 7aee379..0000000
--- a/fetch/api/basic/mode-no-cors.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: no-cors mode and opaque filtering</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="mode-no-cors.js?pipe=sub"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/mode-no-cors.js b/fetch/api/basic/mode-no-cors.js
deleted file mode 100644
index 53e8490..0000000
--- a/fetch/api/basic/mode-no-cors.js
+++ /dev/null
@@ -1,31 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function fetchNoCors(url, isOpaqueFiltered) {
-  var urlQuery = "?pipe=header(x-is-filtered,value)"
-  promise_test(function(test) {
-    if (isOpaqueFiltered)
-      return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
-        assert_equals(resp.status, 0, "Opaque filter: status is 0");
-        assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
-        assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
-        assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered");
-      });
-    else
-      return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
-        assert_equals(resp.status, 200, "HTTP status is 200");
-        assert_equals(resp.type , "basic", "Response's type is basic");
-        assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered");
-      });
-  }, "Fetch "+ url + " with no-cors mode");
-}
-
-fetchNoCors(RESOURCES_DIR + "top.txt", false);
-fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false);
-fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true);
-fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true);
-
-done();
-
diff --git a/fetch/api/basic/mode-no-cors.sub.any.js b/fetch/api/basic/mode-no-cors.sub.any.js
new file mode 100644
index 0000000..709eef5
--- /dev/null
+++ b/fetch/api/basic/mode-no-cors.sub.any.js
@@ -0,0 +1,28 @@
+// META: script=../resources/utils.js
+
+function fetchNoCors(url, isOpaqueFiltered) {
+  var urlQuery = "?pipe=header(x-is-filtered,value)"
+  promise_test(function(test) {
+    if (isOpaqueFiltered)
+      return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+        assert_equals(resp.status, 0, "Opaque filter: status is 0");
+        assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+        assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+        assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered");
+      });
+    else
+      return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+        assert_equals(resp.status, 200, "HTTP status is 200");
+        assert_equals(resp.type , "basic", "Response's type is basic");
+        assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered");
+      });
+  }, "Fetch "+ url + " with no-cors mode");
+}
+
+fetchNoCors(RESOURCES_DIR + "top.txt", false);
+fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false);
+fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true);
+fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true);
+
+done();
+
diff --git a/fetch/api/basic/response-url-worker.html b/fetch/api/basic/response-url-worker.html
deleted file mode 100644
index 03374e0..0000000
--- a/fetch/api/basic/response-url-worker.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: response url getter</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#response-class">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-    fetch_tests_from_worker(new Worker("response-url.js?pipe=sub"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/basic/response-url.html b/fetch/api/basic/response-url.html
deleted file mode 100644
index dfe9d96..0000000
--- a/fetch/api/basic/response-url.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: response url getter</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#response-class">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="response-url.js?pipe=sub"></script>
-  </body>
-</html>
diff --git a/fetch/api/basic/response-url.js b/fetch/api/basic/response-url.js
deleted file mode 100644
index 91b553a..0000000
--- a/fetch/api/basic/response-url.js
+++ /dev/null
@@ -1,21 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-}
-
-function checkResponseURL(fetchedURL, expectedURL)
-{
-    promise_test(function() {
-        return fetch(fetchedURL).then(function(response) {
-            assert_equals(response.url, expectedURL);
-        });
-    }, "Testing response url getter with " +fetchedURL);
-}
-
-var baseURL = "http://{{host}}:{{ports[http][0]}}";
-checkResponseURL(baseURL + "/ada", baseURL + "/ada");
-checkResponseURL(baseURL + "/#", baseURL + "/");
-checkResponseURL(baseURL + "/#ada", baseURL + "/");
-checkResponseURL(baseURL + "#ada", baseURL + "/");
-
-done();
-
diff --git a/fetch/api/basic/response-url.sub.any.js b/fetch/api/basic/response-url.sub.any.js
new file mode 100644
index 0000000..0d123c4
--- /dev/null
+++ b/fetch/api/basic/response-url.sub.any.js
@@ -0,0 +1,16 @@
+function checkResponseURL(fetchedURL, expectedURL)
+{
+    promise_test(function() {
+        return fetch(fetchedURL).then(function(response) {
+            assert_equals(response.url, expectedURL);
+        });
+    }, "Testing response url getter with " +fetchedURL);
+}
+
+var baseURL = "http://{{host}}:{{ports[http][0]}}";
+checkResponseURL(baseURL + "/ada", baseURL + "/ada");
+checkResponseURL(baseURL + "/#", baseURL + "/");
+checkResponseURL(baseURL + "/#ada", baseURL + "/");
+checkResponseURL(baseURL + "#ada", baseURL + "/");
+
+done();
diff --git a/fetch/api/basic/scheme-about.any.js b/fetch/api/basic/scheme-about.any.js
index ee0572f..aae7146 100644
--- a/fetch/api/basic/scheme-about.any.js
+++ b/fetch/api/basic/scheme-about.any.js
@@ -1,34 +1,17 @@
 // META: script=../resources/utils.js
 
-function checkFetchResponse(url, method, desc) {
-  if (!desc) {
-    var cut = (url.length >= 40) ? "[...]" : "";
-    cut += " (" + method + ")"
-    desc = "Fetching " + url.substring(0, 40) + cut + " is OK"
-  }
+function checkNetworkError(url, method) {
+  method = method || "GET";
+  const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO"
   promise_test(function(test) {
-    return fetch(url, { method: method }).then(function(resp) {
-     assert_equals(resp.status, 200, "HTTP status is 200");
-     assert_equals(resp.type, "basic", "response type is basic");
-     assert_equals(resp.headers.get("Content-Type"), "text/html;charset=utf-8", "Content-Type is " + resp.headers.get("Content-Type"));
-     return resp.text();
-    })
-  }, desc);
-}
-
-checkFetchResponse("about:blank", "GET");
-checkFetchResponse("about:blank", "PUT");
-checkFetchResponse("about:blank", "POST");
-
-function checkKoUrl(url, desc) {
-  if (!desc)
-    desc = "Fetching " + url.substring(0, 45) + " is KO"
-  promise_test(function(test) {
-    var promise = fetch(url);
+    var promise = fetch(url, { method: method });
     return promise_rejects(test, new TypeError(), promise);
   }, desc);
 }
 
-checkKoUrl("about:invalid.com");
-checkKoUrl("about:config");
-checkKoUrl("about:unicorn");
+checkNetworkError("about:blank", "GET");
+checkNetworkError("about:blank", "PUT");
+checkNetworkError("about:blank", "POST");
+checkNetworkError("about:invalid.com");
+checkNetworkError("about:config");
+checkNetworkError("about:unicorn");
diff --git a/fetch/api/basic/scheme-blob-worker.html b/fetch/api/basic/scheme-blob-worker.html
deleted file mode 100644
index 961ecbd..0000000
--- a/fetch/api/basic/scheme-blob-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: blob scheme</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#basic-fetch">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("scheme-blob.js?pipe=sub"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/scheme-blob.html b/fetch/api/basic/scheme-blob.html
deleted file mode 100644
index 7787c37..0000000
--- a/fetch/api/basic/scheme-blob.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: blob scheme</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#basic-fetch">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="scheme-blob.js?pipe=sub"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/scheme-blob.js b/fetch/api/basic/scheme-blob.js
deleted file mode 100644
index 9bf73a6..0000000
--- a/fetch/api/basic/scheme-blob.js
+++ /dev/null
@@ -1,48 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function checkFetchResponse(url, data, mime, size, desc) {
-  promise_test(function(test) {
-    size = size.toString();
-    return fetch(url).then(function(resp) {
-      assert_equals(resp.status, 200, "HTTP status is 200");
-      assert_equals(resp.type, "basic", "response type is basic");
-      assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
-      assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length"));
-      return resp.text();
-    }).then(function(bodyAsText) {
-      assert_equals(bodyAsText, data, "Response's body is " + data);
-    });
-  }, desc);
-}
-
-var blob = new Blob(["Blob's data"], { "type" : "text/plain" });
-checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain",  blob.size,
-                  "Fetching [GET] URL.createObjectURL(blob) is OK");
-
-function checkKoUrl(url, method, desc) {
-  promise_test(function(test) {
-    var promise = fetch(url, {"method": method});
-    return promise_rejects(test, new TypeError(), promise);
-  }, desc);
-}
-
-var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" });
-checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET",
-          "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO");
-
-var invalidRequestMethods = [
-  "POST",
-  "OPTIONS",
-  "HEAD",
-  "PUT",
-  "DELETE",
-  "INVALID",
-];
-invalidRequestMethods.forEach(function(method) {
-  checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO");
-});
-
-done();
diff --git a/fetch/api/basic/scheme-blob.sub.any.js b/fetch/api/basic/scheme-blob.sub.any.js
new file mode 100644
index 0000000..fb1357e
--- /dev/null
+++ b/fetch/api/basic/scheme-blob.sub.any.js
@@ -0,0 +1,45 @@
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, size, desc) {
+  promise_test(function(test) {
+    size = size.toString();
+    return fetch(url).then(function(resp) {
+      assert_equals(resp.status, 200, "HTTP status is 200");
+      assert_equals(resp.type, "basic", "response type is basic");
+      assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+      assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length"));
+      return resp.text();
+    }).then(function(bodyAsText) {
+      assert_equals(bodyAsText, data, "Response's body is " + data);
+    });
+  }, desc);
+}
+
+var blob = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain",  blob.size,
+                  "Fetching [GET] URL.createObjectURL(blob) is OK");
+
+function checkKoUrl(url, method, desc) {
+  promise_test(function(test) {
+    var promise = fetch(url, {"method": method});
+    return promise_rejects(test, new TypeError(), promise);
+  }, desc);
+}
+
+var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET",
+          "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO");
+
+var invalidRequestMethods = [
+  "POST",
+  "OPTIONS",
+  "HEAD",
+  "PUT",
+  "DELETE",
+  "INVALID",
+];
+invalidRequestMethods.forEach(function(method) {
+  checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO");
+});
+
+done();
diff --git a/fetch/api/basic/scheme-others-worker.html b/fetch/api/basic/scheme-others-worker.html
deleted file mode 100644
index 397d925..0000000
--- a/fetch/api/basic/scheme-others-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: urls with unsupported schemes</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#basic-fetch">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("scheme-others.js?pipe=sub"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/scheme-others.html b/fetch/api/basic/scheme-others.html
deleted file mode 100644
index dd37143..0000000
--- a/fetch/api/basic/scheme-others.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: urls with unsupported schemes</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#basic-fetch">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="scheme-others.js?pipe=sub"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/basic/scheme-others.js b/fetch/api/basic/scheme-others.js
deleted file mode 100644
index ce02ec1..0000000
--- a/fetch/api/basic/scheme-others.js
+++ /dev/null
@@ -1,33 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function checkKoUrl(url, desc) {
-  if (!desc)
-    desc = "Fetching " + url.substring(0, 45) + " is KO"
-  promise_test(function(test) {
-    var promise = fetch(url);
-    return promise_rejects(test, new TypeError(), promise);
-  }, desc);
-}
-
-var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/";
-checkKoUrl("aaa" + urlWithoutScheme);
-checkKoUrl("cap" + urlWithoutScheme);
-checkKoUrl("cid" + urlWithoutScheme);
-checkKoUrl("dav" + urlWithoutScheme);
-checkKoUrl("dict" + urlWithoutScheme);
-checkKoUrl("dns" + urlWithoutScheme);
-checkKoUrl("geo" + urlWithoutScheme);
-checkKoUrl("im" + urlWithoutScheme);
-checkKoUrl("imap" + urlWithoutScheme);
-checkKoUrl("ipp" + urlWithoutScheme);
-checkKoUrl("ldap" + urlWithoutScheme);
-checkKoUrl("mailto" + urlWithoutScheme);
-checkKoUrl("nfs" + urlWithoutScheme);
-checkKoUrl("pop" + urlWithoutScheme);
-checkKoUrl("rtsp" + urlWithoutScheme);
-checkKoUrl("snmp" + urlWithoutScheme);
-
-done();
diff --git a/fetch/api/basic/scheme-others.sub.any.js b/fetch/api/basic/scheme-others.sub.any.js
new file mode 100644
index 0000000..5f9848ff
--- /dev/null
+++ b/fetch/api/basic/scheme-others.sub.any.js
@@ -0,0 +1,30 @@
+// META: script=../resources/utils.js
+
+function checkKoUrl(url, desc) {
+  if (!desc)
+    desc = "Fetching " + url.substring(0, 45) + " is KO"
+  promise_test(function(test) {
+    var promise = fetch(url);
+    return promise_rejects(test, new TypeError(), promise);
+  }, desc);
+}
+
+var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/";
+checkKoUrl("aaa" + urlWithoutScheme);
+checkKoUrl("cap" + urlWithoutScheme);
+checkKoUrl("cid" + urlWithoutScheme);
+checkKoUrl("dav" + urlWithoutScheme);
+checkKoUrl("dict" + urlWithoutScheme);
+checkKoUrl("dns" + urlWithoutScheme);
+checkKoUrl("geo" + urlWithoutScheme);
+checkKoUrl("im" + urlWithoutScheme);
+checkKoUrl("imap" + urlWithoutScheme);
+checkKoUrl("ipp" + urlWithoutScheme);
+checkKoUrl("ldap" + urlWithoutScheme);
+checkKoUrl("mailto" + urlWithoutScheme);
+checkKoUrl("nfs" + urlWithoutScheme);
+checkKoUrl("pop" + urlWithoutScheme);
+checkKoUrl("rtsp" + urlWithoutScheme);
+checkKoUrl("snmp" + urlWithoutScheme);
+
+done();
diff --git a/fetch/api/cors/cors-expose-star-worker.html b/fetch/api/cors/cors-expose-star-worker.html
deleted file mode 100644
index db9b943..0000000
--- a/fetch/api/cors/cors-expose-star-worker.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: Access-Control-Expose-Headers: *</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("cors-expose-star.js?pipe=sub"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/cors/cors-expose-star.html b/fetch/api/cors/cors-expose-star.html
deleted file mode 100644
index 37223bd..0000000
--- a/fetch/api/cors/cors-expose-star.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: Access-Control-Expose-Headers: *</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="cors-expose-star.js?pipe=sub"></script>
-  </body>
-</html>
diff --git a/fetch/api/cors/cors-expose-star.js b/fetch/api/cors/cors-expose-star.js
deleted file mode 100644
index edf4d42..0000000
--- a/fetch/api/cors/cors-expose-star.js
+++ /dev/null
@@ -1,44 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt",
-      sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|"
-
-promise_test(() => {
-  const headers = "header(Access-Control-Allow-Origin,*)"
-  return fetch(url + sharedHeaders + headers).then(resp => {
-    assert_equals(resp.status, 200)
-    assert_equals(resp.type , "cors")
-    assert_equals(resp.headers.get("test"), "X")
-    assert_equals(resp.headers.get("set-cookie"), null)
-    assert_equals(resp.headers.get("*"), "whoa")
-  })
-}, "Basic Access-Control-Expose-Headers: * support")
-
-promise_test(() => {
-  const origin = location.origin, // assuming an ASCII origin
-        headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
-  return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => {
-    assert_equals(resp.status, 200)
-    assert_equals(resp.type , "cors")
-    assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted
-    assert_equals(resp.headers.get("test"), null)
-    assert_equals(resp.headers.get("set-cookie"), null)
-    assert_equals(resp.headers.get("*"), "whoa")
-  })
-}, "* for credentialed fetches only matches literally")
-
-promise_test(() => {
-  const headers =  "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
-  return fetch(url + sharedHeaders + headers).then(resp => {
-    assert_equals(resp.status, 200)
-    assert_equals(resp.type , "cors")
-    assert_equals(resp.headers.get("test"), "X")
-    assert_equals(resp.headers.get("set-cookie"), null)
-    assert_equals(resp.headers.get("*"), "whoa")
-  })
-}, "* can be one of several values")
-
-done();
diff --git a/fetch/api/cors/cors-expose-star.sub.any.js b/fetch/api/cors/cors-expose-star.sub.any.js
new file mode 100644
index 0000000..340e99a
--- /dev/null
+++ b/fetch/api/cors/cors-expose-star.sub.any.js
@@ -0,0 +1,41 @@
+// META: script=../resources/utils.js
+
+const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt",
+      sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|"
+
+promise_test(() => {
+  const headers = "header(Access-Control-Allow-Origin,*)"
+  return fetch(url + sharedHeaders + headers).then(resp => {
+    assert_equals(resp.status, 200)
+    assert_equals(resp.type , "cors")
+    assert_equals(resp.headers.get("test"), "X")
+    assert_equals(resp.headers.get("set-cookie"), null)
+    assert_equals(resp.headers.get("*"), "whoa")
+  })
+}, "Basic Access-Control-Expose-Headers: * support")
+
+promise_test(() => {
+  const origin = location.origin, // assuming an ASCII origin
+        headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
+  return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => {
+    assert_equals(resp.status, 200)
+    assert_equals(resp.type , "cors")
+    assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted
+    assert_equals(resp.headers.get("test"), null)
+    assert_equals(resp.headers.get("set-cookie"), null)
+    assert_equals(resp.headers.get("*"), "whoa")
+  })
+}, "* for credentialed fetches only matches literally")
+
+promise_test(() => {
+  const headers =  "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
+  return fetch(url + sharedHeaders + headers).then(resp => {
+    assert_equals(resp.status, 200)
+    assert_equals(resp.type , "cors")
+    assert_equals(resp.headers.get("test"), "X")
+    assert_equals(resp.headers.get("set-cookie"), null)
+    assert_equals(resp.headers.get("*"), "whoa")
+  })
+}, "* can be one of several values")
+
+done();
diff --git a/fetch/api/cors/cors-multiple-origins-worker.html b/fetch/api/cors/cors-multiple-origins-worker.html
deleted file mode 100644
index a8e5057..0000000
--- a/fetch/api/cors/cors-multiple-origins-worker.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: check multiple Access-Control-Allow-Origin header management</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-cors-protocol">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#cors-check">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("cors-multiple-origins.js?pipe=sub"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/cors/cors-multiple-origins.html b/fetch/api/cors/cors-multiple-origins.html
deleted file mode 100644
index 9b12b05..0000000
--- a/fetch/api/cors/cors-multiple-origins.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: check multiple Access-Control-Allow-Origin header management</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-cors-protocol">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#cors-check">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="cors-multiple-origins.js?pipe=sub"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/cors/cors-multiple-origins.js b/fetch/api/cors/cors-multiple-origins.js
deleted file mode 100644
index e4cf724..0000000
--- a/fetch/api/cors/cors-multiple-origins.js
+++ /dev/null
@@ -1,32 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function corsMultipleOrigins(desc, originList, shouldPass) {
-  var urlParameters = "?origin=" + encodeURIComponent(originList.join(", "));
-  var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
-
-  if (shouldPass) {
-    promise_test(function(test) {
-      return fetch(url + urlParameters).then(function(resp) {
-        assert_equals(resp.status, 200, "Response's status is 200");
-      });
-    }, desc);
-  } else {
-    promise_test(function(test) {
-      return promise_rejects(test, new TypeError(), fetch(url + urlParameters));
-    }, desc);
-  }
-}
-/* Actual origin */
-var origin = "http://{{host}}:{{ports[http][0]}}";
-
-corsMultipleOrigins("3 origins allowed, match the 3rd (" + origin + ")", ["\"\"", "http://example.com", origin], true);
-corsMultipleOrigins("3 origins allowed, match the 3rd (\"*\")", ["\"\"", "http://example.com", "*"], true);
-corsMultipleOrigins("3 origins allowed, match twice (" + origin + ")", ["\"\"", origin, origin], true);
-corsMultipleOrigins("3 origins allowed, match twice (\"*\")", ["*", "http://example.com", "*"], true);
-corsMultipleOrigins("3 origins allowed, match twice (\"*\" and " + origin + ")", ["*", "http://example.com", origin], true);
-corsMultipleOrigins("3 origins allowed, no match", ["", "http://example.com", "https://example2.com"], false);
-
-done();
diff --git a/fetch/api/cors/cors-multiple-origins.sub.any.js b/fetch/api/cors/cors-multiple-origins.sub.any.js
new file mode 100644
index 0000000..f32b387
--- /dev/null
+++ b/fetch/api/cors/cors-multiple-origins.sub.any.js
@@ -0,0 +1,21 @@
+// META: script=../resources/utils.js
+
+function corsMultipleOrigins(originList) {
+  var urlParameters = "?origin=" + encodeURIComponent(originList.join(", "));
+  var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+  promise_test(function(test) {
+    return promise_rejects(test, new TypeError(), fetch(url + urlParameters));
+  }, "Listing multiple origins is illegal: " + originList);
+}
+/* Actual origin */
+var origin = "http://{{host}}:{{ports[http][0]}}";
+
+corsMultipleOrigins(["\"\"", "http://example.com", origin]);
+corsMultipleOrigins(["\"\"", "http://example.com", "*"]);
+corsMultipleOrigins(["\"\"", origin, origin]);
+corsMultipleOrigins(["*", "http://example.com", "*"]);
+corsMultipleOrigins(["*", "http://example.com", origin]);
+corsMultipleOrigins(["", "http://example.com", "https://example2.com"]);
+
+done();
diff --git a/fetch/api/headers/header-values-normalize.html b/fetch/api/headers/header-values-normalize.html
index 616d83a..30e7a58 100644
--- a/fetch/api/headers/header-values-normalize.html
+++ b/fetch/api/headers/header-values-normalize.html
@@ -56,9 +56,9 @@
   promise_test((t) => {
     if(fail) {
       return Promise.all([
-        promise_rejects(t, new TypeError(), fetch("about:blank", { headers: {"val1": val1} })),
-        promise_rejects(t, new TypeError(), fetch("about:blank", { headers: {"val2": val2} })),
-        promise_rejects(t, new TypeError(), fetch("about:blank", { headers: {"val3": val3} }))
+        promise_rejects(t, new TypeError(), fetch(url, { headers: {"val1": val1} })),
+        promise_rejects(t, new TypeError(), fetch(url, { headers: {"val2": val2} })),
+        promise_rejects(t, new TypeError(), fetch(url, { headers: {"val3": val3} }))
       ])
     } else {
       return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => {
diff --git a/fetch/api/headers/header-values.html b/fetch/api/headers/header-values.html
index 35e7b7e..6dfe0d3 100644
--- a/fetch/api/headers/header-values.html
+++ b/fetch/api/headers/header-values.html
@@ -15,7 +15,7 @@
     assert_throws("SyntaxError", () => xhr.setRequestHeader("value-test", val))
   }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw")
 
-  promise_test(t => promise_rejects(t, new TypeError(), fetch("about:blank", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw")
+  promise_test(t => promise_rejects(t, new TypeError(), fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw")
 })
 
 // Valid values
diff --git a/fetch/api/headers/headers-idl.html b/fetch/api/headers/headers-idl.html
deleted file mode 100644
index 078c9d0..0000000
--- a/fetch/api/headers/headers-idl.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Headers idl interface</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#response">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <script src="/resources/WebIDLParser.js"></script>
-    <script src="/resources/idlharness.js"></script>
-  </head>
-  <body>
-    <script id="headers-idl" type="text/plain">
-      typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;
-
-      [Constructor(optional HeadersInit init),
-       Exposed=(Window,Worker)]
-      interface Headers {
-        void append(ByteString name, ByteString value);
-        void delete(ByteString name);
-        ByteString? get(ByteString name);
-        boolean has(ByteString name);
-        void set(ByteString name, ByteString value);
-        iterable<ByteString, ByteString>;
-      };
-    </script>
-    <script>
-      var idlsArray = new IdlArray();
-      var idl = document.getElementById("headers-idl").textContent
-      idlsArray.add_idls(idl);
-      idlsArray.add_objects({ Headers: ['new Headers()'] });
-      idlsArray.test();
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/idl.any.js b/fetch/api/idl.any.js
new file mode 100644
index 0000000..cae5ca3
--- /dev/null
+++ b/fetch/api/idl.any.js
@@ -0,0 +1,18 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+promise_test(async() => {
+  const text = await (await fetch("/interfaces/fetch.idl")).text();
+  const idl_array = new IdlArray();
+  idl_array.add_idls(text);
+  idl_array.add_untested_idls("[Exposed=(Window,Worker)] interface AbortSignal {};");
+  idl_array.add_untested_idls("[Exposed=(Window,Worker)] interface ReadableStream {};");
+  idl_array.add_untested_idls("enum ReferrerPolicy {};");
+  idl_array.add_objects({
+    Headers: ["new Headers()"],
+    Request: ["new Request('about:blank')"],
+    Response: ["new Response()"],
+  });
+  idl_array.test();
+}, "Fetch Standard IDL");
diff --git a/fetch/api/redirect/redirect-count-worker.html b/fetch/api/redirect/redirect-count-worker.html
deleted file mode 100644
index 0f624e8..0000000
--- a/fetch/api/redirect/redirect-count-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: rediraction loop</title>
-    <meta name="timeout" content="long">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-count.js"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-count.any.js b/fetch/api/redirect/redirect-count.any.js
new file mode 100644
index 0000000..7fa6dd1
--- /dev/null
+++ b/fetch/api/redirect/redirect-count.any.js
@@ -0,0 +1,40 @@
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+
+function redirectCount(desc, redirectUrl, redirectLocation, redirectStatus, maxCount, shouldPass) {
+  var uuid_token = token();
+
+  var urlParameters = "?token=" + uuid_token + "&max_age=0";
+  urlParameters += "&redirect_status=" + redirectStatus;
+  urlParameters += "&max_count=" + maxCount;
+  if (redirectLocation)
+    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+  var url = redirectUrl;
+  var requestInit = {"redirect": "follow"};
+
+  promise_test(function(test) {
+    return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+      assert_equals(resp.status, 200, "Clean stash response's status is 200");
+
+      if (!shouldPass)
+        return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
+
+      return fetch(url + urlParameters, requestInit).then(function(resp) {
+        assert_equals(resp.status, 200, "Response's status is 200");
+        return resp.text();
+      }).then(function(body) {
+        assert_equals(body, maxCount.toString(), "Redirected " + maxCount + " times");
+      });
+    });
+  }, desc);
+}
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+
+for (var statusCode of [301, 302, 303, 307, 308]) {
+  redirectCount("Redirect " + statusCode + " 20 times", redirUrl, redirUrl, statusCode, 20, true);
+  redirectCount("Redirect " + statusCode + " 21 times", redirUrl, redirUrl, statusCode, 21, false);
+}
+
+done();
diff --git a/fetch/api/redirect/redirect-count.html b/fetch/api/redirect/redirect-count.html
deleted file mode 100644
index d6a66e6..0000000
--- a/fetch/api/redirect/redirect-count.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: redirection loop</title>
-    <meta name="timeout" content="long">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="/common/utils.js"></script>
-    <script src="../resources/utils.js"></script>
-    <script src="redirect-count.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-count.js b/fetch/api/redirect/redirect-count.js
deleted file mode 100644
index 0103280..0000000
--- a/fetch/api/redirect/redirect-count.js
+++ /dev/null
@@ -1,43 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-  importScripts("/common/utils.js");
-}
-
-function redirectCount(desc, redirectUrl, redirectLocation, redirectStatus, maxCount, shouldPass) {
-  var uuid_token = token();
-
-  var urlParameters = "?token=" + uuid_token + "&max_age=0";
-  urlParameters += "&redirect_status=" + redirectStatus;
-  urlParameters += "&max_count=" + maxCount;
-  if (redirectLocation)
-    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
-
-  var url = redirectUrl;
-  var requestInit = {"redirect": "follow"};
-
-  promise_test(function(test) {
-    return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
-      assert_equals(resp.status, 200, "Clean stash response's status is 200");
-
-      if (!shouldPass)
-        return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
-
-      return fetch(url + urlParameters, requestInit).then(function(resp) {
-        assert_equals(resp.status, 200, "Response's status is 200");
-        return resp.text();
-      }).then(function(body) {
-        assert_equals(body, maxCount.toString(), "Redirected " + maxCount + " times");
-      });
-    });
-  }, desc);
-}
-
-var redirUrl = RESOURCES_DIR + "redirect.py";
-
-for (var statusCode of [301, 302, 303, 307, 308]) {
-  redirectCount("Redirect " + statusCode + " 20 times", redirUrl, redirUrl, statusCode, 20, true);
-  redirectCount("Redirect " + statusCode + " 21 times", redirUrl, redirUrl, statusCode, 21, false);
-}
-
-done();
diff --git a/fetch/api/redirect/redirect-empty-location-worker.html b/fetch/api/redirect/redirect-empty-location-worker.html
deleted file mode 100644
index 7dce98c..0000000
--- a/fetch/api/redirect/redirect-empty-location-worker.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: handling empty Location header during redirection</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-empty-location.js"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-empty-location.any.js b/fetch/api/redirect/redirect-empty-location.any.js
new file mode 100644
index 0000000..ace8f22
--- /dev/null
+++ b/fetch/api/redirect/redirect-empty-location.any.js
@@ -0,0 +1,20 @@
+// META: script=../resources/utils.js
+
+// Tests receiving a redirect response with a Location header with an empty
+// value.
+
+const url = RESOURCES_DIR + 'redirect-empty-location.py';
+
+promise_test(t => {
+  return promise_rejects(t, new TypeError(), fetch(url, {redirect:'follow'}));
+}, 'redirect response with empty Location, follow mode');
+
+promise_test(t => {
+  return fetch(url, {redirect:'manual'})
+    .then(resp => {
+      assert_equals(resp.type, 'opaqueredirect');
+      assert_equals(resp.status, 0);
+    });
+}, 'redirect response with empty Location, manual mode');
+
+done();
diff --git a/fetch/api/redirect/redirect-empty-location.html b/fetch/api/redirect/redirect-empty-location.html
deleted file mode 100644
index afb033b..0000000
--- a/fetch/api/redirect/redirect-empty-location.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: handling empty Location header during redirection</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="redirect-empty-location.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-empty-location.js b/fetch/api/redirect/redirect-empty-location.js
deleted file mode 100644
index a9d03c5..0000000
--- a/fetch/api/redirect/redirect-empty-location.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// Tests receiving a redirect response with a Location header with an empty
-// value.
-
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-const url = RESOURCES_DIR + 'redirect-empty-location.py';
-
-promise_test(t => {
-  return promise_rejects(t, new TypeError(), fetch(url, {redirect:'follow'}));
-}, 'redirect response with empty Location, follow mode');
-
-promise_test(t => {
-  return fetch(url, {redirect:'manual'})
-    .then(resp => {
-      assert_equals(resp.type, 'opaqueredirect');
-      assert_equals(resp.status, 0);
-    });
-}, 'redirect response with empty Location, manual mode');
-
-done();
diff --git a/fetch/api/redirect/redirect-location-worker.html b/fetch/api/redirect/redirect-location-worker.html
deleted file mode 100644
index e297081..0000000
--- a/fetch/api/redirect/redirect-location-worker.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: handling Location header during redirection</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-location.js"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/redirect/redirect-location.any.js b/fetch/api/redirect/redirect-location.any.js
new file mode 100644
index 0000000..27baefc
--- /dev/null
+++ b/fetch/api/redirect/redirect-location.any.js
@@ -0,0 +1,47 @@
+// META: script=../resources/utils.js
+
+function redirectLocation(desc, redirectUrl, redirectLocation, redirectStatus, redirectMode, shouldPass) {
+  var url = redirectUrl;
+  var urlParameters = "?redirect_status=" + redirectStatus;
+  if (redirectLocation)
+    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+  var requestInit = {"redirect": redirectMode};
+
+  promise_test(function(test) {
+    if (redirectMode === "error" || !shouldPass)
+      return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
+    if (redirectMode === "manual")
+      return fetch(url + urlParameters, requestInit).then(function(resp) {
+        assert_equals(resp.status, 0, "Response's status is 0");
+        assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+        assert_equals(resp.statusText, "", "Response's statusText is \"\"");
+        assert_true(resp.headers.entries().next().done, "Headers should be empty");
+      });
+
+    if (redirectMode === "follow")
+      return fetch(url + urlParameters, requestInit).then(function(resp) {
+        assert_equals(resp.status, redirectStatus, "Response's status is " + redirectStatus);
+      });
+    assert_unreached(redirectMode + " is not a valid redirect mode");
+  }, desc);
+}
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = "top.txt";
+var invalidLocationUrl = "invalidurl:";
+var dataLocationUrl = "data:,data%20url";
+// FIXME: We may want to mix redirect-mode and cors-mode.
+// FIXME: Add tests for "error" redirect-mode.
+for (var statusCode of [301, 302, 303, 307, 308]) {
+  redirectLocation("Redirect " + statusCode + " in \"follow\" mode without location", redirUrl, undefined, statusCode, "follow", true);
+  redirectLocation("Redirect " + statusCode + " in \"manual\" mode without location", redirUrl, undefined, statusCode, "manual", true);
+
+  redirectLocation("Redirect " + statusCode + " in \"follow\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "follow", false);
+  redirectLocation("Redirect " + statusCode + " in \"manual\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "manual", true);
+
+  redirectLocation("Redirect " + statusCode + " in \"follow\" mode with data location", redirUrl, dataLocationUrl, statusCode, "follow", false);
+  redirectLocation("Redirect " + statusCode + " in \"manual\" mode with data location", redirUrl, dataLocationUrl, statusCode, "manual", true);
+}
+
+done();
diff --git a/fetch/api/redirect/redirect-location.html b/fetch/api/redirect/redirect-location.html
deleted file mode 100644
index ac35dea..0000000
--- a/fetch/api/redirect/redirect-location.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: handling Location header during redirection</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="redirect-location.js"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/redirect/redirect-location.js b/fetch/api/redirect/redirect-location.js
deleted file mode 100644
index 5b9a4fe..0000000
--- a/fetch/api/redirect/redirect-location.js
+++ /dev/null
@@ -1,50 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-function redirectLocation(desc, redirectUrl, redirectLocation, redirectStatus, redirectMode, shouldPass) {
-  var url = redirectUrl;
-  var urlParameters = "?redirect_status=" + redirectStatus;
-  if (redirectLocation)
-    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
-
-  var requestInit = {"redirect": redirectMode};
-
-  promise_test(function(test) {
-    if (redirectMode === "error" || !shouldPass)
-      return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
-    if (redirectMode === "manual")
-      return fetch(url + urlParameters, requestInit).then(function(resp) {
-        assert_equals(resp.status, 0, "Response's status is 0");
-        assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
-        assert_equals(resp.statusText, "", "Response's statusText is \"\"");
-        assert_true(resp.headers.entries().next().done, "Headers should be empty");
-      });
-
-    if (redirectMode === "follow")
-      return fetch(url + urlParameters, requestInit).then(function(resp) {
-        assert_equals(resp.status, redirectStatus, "Response's status is " + redirectStatus);
-      });
-    assert_unreached(redirectMode + " is not a valid redirect mode");
-  }, desc);
-}
-
-var redirUrl = RESOURCES_DIR + "redirect.py";
-var locationUrl = "top.txt";
-var invalidLocationUrl = "invalidurl:";
-var dataLocationUrl = "data:,data%20url";
-// FIXME: We may want to mix redirect-mode and cors-mode.
-// FIXME: Add tests for "error" redirect-mode.
-for (var statusCode of [301, 302, 303, 307, 308]) {
-  redirectLocation("Redirect " + statusCode + " in \"follow\" mode without location", redirUrl, undefined, statusCode, "follow", true);
-  redirectLocation("Redirect " + statusCode + " in \"manual\" mode without location", redirUrl, undefined, statusCode, "manual", true);
-
-  redirectLocation("Redirect " + statusCode + " in \"follow\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "follow", false);
-  redirectLocation("Redirect " + statusCode + " in \"manual\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "manual", true);
-
-  redirectLocation("Redirect " + statusCode + " in \"follow\" mode with data location", redirUrl, dataLocationUrl, statusCode, "follow", false);
-  redirectLocation("Redirect " + statusCode + " in \"manual\" mode with data location", redirUrl, dataLocationUrl, statusCode, "manual", true);
-}
-
-done();
diff --git a/fetch/api/redirect/redirect-method-worker.html b/fetch/api/redirect/redirect-method-worker.html
deleted file mode 100644
index fc0bc5e..0000000
--- a/fetch/api/redirect/redirect-method-worker.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: request method handling when redirected</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-method.js"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/redirect/redirect-method.any.js b/fetch/api/redirect/redirect-method.any.js
new file mode 100644
index 0000000..056ccf8
--- /dev/null
+++ b/fetch/api/redirect/redirect-method.any.js
@@ -0,0 +1,68 @@
+// META: script=../resources/utils.js
+
+// Creates a promise_test that fetches a URL that returns a redirect response.
+//
+// |opts| has additional options:
+// |opts.body|: the request body as a string or blob (default is empty body)
+// |opts.expectedBodyAsString|: the expected response body as a string. The
+// server is expected to echo the request body. The default is the empty string
+// if the request after redirection isn't POST; otherwise it's |opts.body|.
+function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) {
+  var url = redirectUrl;
+  var urlParameters = "?redirect_status=" + redirectStatus;
+  urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+  var requestInit = {"method": method, "redirect": "follow"};
+  opts = opts || {};
+  if (opts.body)
+    requestInit.body = opts.body;
+
+  promise_test(function(test) {
+    return fetch(url + urlParameters, requestInit).then(function(resp) {
+      assert_equals(resp.status, 200, "Response's status is 200");
+      assert_equals(resp.type, "basic", "Response's type basic");
+      assert_equals(resp.headers.get("x-request-method"), expectedMethod, "Request method after redirection is " + expectedMethod);
+      assert_true(resp.redirected);
+      return resp.text().then(function(text) {
+        let expectedBody = "";
+        if (expectedMethod == "POST")
+          expectedBody = opts.expectedBodyAsString || requestInit.body;
+        assert_equals(text, expectedBody, "request body");
+      });
+    });
+  }, desc);
+}
+
+promise_test(function(test) {
+  assert_false(new Response().redirected);
+  return fetch(RESOURCES_DIR + "method.py").then(function(resp) {
+    assert_equals(resp.status, 200, "Response's status is 200");
+    assert_false(resp.redirected);
+  });
+}, "Response.redirected should be false on not-redirected responses");
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = "method.py";
+
+const stringBody = "this is my body";
+const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]);
+const blobBodyAsString = "it's me the blob! and more blob!";
+
+redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET");
+redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody });
+redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD");
+
+redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET");
+redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody });
+redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD");
+
+redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET");
+redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody });
+redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD");
+
+redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET");
+redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody });
+redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString });
+redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD");
+
+done();
diff --git a/fetch/api/redirect/redirect-method.html b/fetch/api/redirect/redirect-method.html
deleted file mode 100644
index 028842d..0000000
--- a/fetch/api/redirect/redirect-method.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: request method handling when redirected</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="../resources/utils.js"></script>
-    <script src="redirect-method.js"></script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/redirect/redirect-method.js b/fetch/api/redirect/redirect-method.js
deleted file mode 100644
index 0a7f2df..0000000
--- a/fetch/api/redirect/redirect-method.js
+++ /dev/null
@@ -1,71 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-}
-
-// Creates a promise_test that fetches a URL that returns a redirect response.
-//
-// |opts| has additional options:
-// |opts.body|: the request body as a string or blob (default is empty body)
-// |opts.expectedBodyAsString|: the expected response body as a string. The
-// server is expected to echo the request body. The default is the empty string
-// if the request after redirection isn't POST; otherwise it's |opts.body|.
-function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) {
-  var url = redirectUrl;
-  var urlParameters = "?redirect_status=" + redirectStatus;
-  urlParameters += "&location=" + encodeURIComponent(redirectLocation);
-
-  var requestInit = {"method": method, "redirect": "follow"};
-  opts = opts || {};
-  if (opts.body)
-    requestInit.body = opts.body;
-
-  promise_test(function(test) {
-    return fetch(url + urlParameters, requestInit).then(function(resp) {
-      assert_equals(resp.status, 200, "Response's status is 200");
-      assert_equals(resp.type, "basic", "Response's type basic");
-      assert_equals(resp.headers.get("x-request-method"), expectedMethod, "Request method after redirection is " + expectedMethod);
-      assert_true(resp.redirected);
-      return resp.text().then(function(text) {
-        let expectedBody = "";
-        if (expectedMethod == "POST")
-          expectedBody = opts.expectedBodyAsString || requestInit.body;
-        assert_equals(text, expectedBody, "request body");
-      });
-    });
-  }, desc);
-}
-
-promise_test(function(test) {
-  assert_false(new Response().redirected);
-  return fetch(RESOURCES_DIR + "method.py").then(function(resp) {
-    assert_equals(resp.status, 200, "Response's status is 200");
-    assert_false(resp.redirected);
-  });
-}, "Response.redirected should be false on not-redirected responses");
-
-var redirUrl = RESOURCES_DIR + "redirect.py";
-var locationUrl = "method.py";
-
-const stringBody = "this is my body";
-const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]);
-const blobBodyAsString = "it's me the blob! and more blob!";
-
-redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET");
-redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody });
-redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD");
-
-redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET");
-redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody });
-redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD");
-
-redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET");
-redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody });
-redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD");
-
-redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET");
-redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody });
-redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString });
-redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD");
-
-done();
diff --git a/fetch/api/redirect/redirect-mode-worker.html b/fetch/api/redirect/redirect-mode-worker.html
deleted file mode 100644
index 32d219f..0000000
--- a/fetch/api/redirect/redirect-mode-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: redirect mode handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-mode.js"));
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/fetch/api/redirect/redirect-mode.any.js b/fetch/api/redirect/redirect-mode.any.js
new file mode 100644
index 0000000..7d34fa6
--- /dev/null
+++ b/fetch/api/redirect/redirect-mode.any.js
@@ -0,0 +1,49 @@
+// META: script=/common/get-host-info.sub.js
+
+var redirectLocation = "cors-top.txt";
+
+function testRedirect(origin, redirectStatus, redirectMode, corsMode) {
+  var url = new URL("../resources/redirect.py", self.location);
+  if (origin === "cross-origin") {
+    url.host = get_host_info().REMOTE_HOST;
+  }
+
+  var urlParameters = "?redirect_status=" + redirectStatus;
+  urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+  var requestInit = {redirect: redirectMode, mode: corsMode};
+
+  promise_test(function(test) {
+    if (redirectMode === "error" ||
+        (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin"))
+      return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
+    if (redirectMode === "manual")
+      return fetch(url + urlParameters, requestInit).then(function(resp) {
+        assert_equals(resp.status, 0, "Response's status is 0");
+        assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+        assert_equals(resp.statusText, "", "Response's statusText is \"\"");
+        assert_equals(resp.url, url + urlParameters, "Response URL should be the original one");
+      });
+    if (redirectMode === "follow")
+      return fetch(url + urlParameters, requestInit).then(function(resp) {
+        if (corsMode !== "no-cors" || origin === "same-origin") {
+          assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one");
+          assert_equals(resp.status, 200, "Response's status is 200");
+        } else {
+          assert_equals(resp.type, "opaque", "Response is opaque");
+        }
+      });
+    assert_unreached(redirectMode + " is no a valid redirect mode");
+  }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode");
+}
+
+for (var origin of ["same-origin", "cross-origin"]) {
+  for (var statusCode of [301, 302, 303, 307, 308]) {
+    for (var redirect of ["error", "manual", "follow"]) {
+      for (var mode of ["cors", "no-cors"])
+        testRedirect(origin, statusCode, redirect, mode);
+    }
+  }
+}
+
+done();
diff --git a/fetch/api/redirect/redirect-mode.html b/fetch/api/redirect/redirect-mode.html
deleted file mode 100644
index 20a0cd6..0000000
--- a/fetch/api/redirect/redirect-mode.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: redirect mode handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="/common/get-host-info.sub.js"></script>
-    <script src="redirect-mode.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-mode.js b/fetch/api/redirect/redirect-mode.js
deleted file mode 100644
index b59a8d5..0000000
--- a/fetch/api/redirect/redirect-mode.js
+++ /dev/null
@@ -1,41 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/resources/testharness.js");
-  importScripts("/common/get-host-info.sub.js")
-}
-
-function redirectMode(desc, redirectUrl, redirectLocation, redirectStatus, redirectMode) {
-  var url = redirectUrl;
-  var urlParameters = "?redirect_status=" + redirectStatus;
-  urlParameters += "&location=" + encodeURIComponent(redirectLocation);
-
-  var requestInit = {"redirect": redirectMode};
-
-  promise_test(function(test) {
-    if (redirectMode === "error")
-      return promise_rejects(test, new TypeError(), fetch(url + urlParameters, requestInit));
-    if (redirectMode === "manual")
-      return fetch(url + urlParameters, requestInit).then(function(resp) {
-        assert_equals(resp.status, 0, "Response's status is 0");
-        assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
-        assert_equals(resp.statusText, "", "Response's statusText is \"\"");
-        assert_equals(resp.url, url + urlParameters, "Response URL should be the original one");
-      });
-    if (redirectMode === "follow")
-      return fetch(url + urlParameters, requestInit).then(function(resp) {
-        assert_true(new URL(resp.url).pathname.endsWith(locationUrl), "Response's url should be the redirected one");
-        assert_equals(resp.status, 200, "Response's status is 200");
-      });
-    assert_unreached(redirectMode + " is no a valid redirect mode");
-  }, desc);
-}
-
-var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
-var locationUrl = "top.txt";
-
-for (var statusCode of [301, 302, 303, 307, 308]) {
-  redirectMode("Redirect " + statusCode + " in \"error\" mode ", redirUrl, locationUrl, statusCode, "error");
-  redirectMode("Redirect " + statusCode + " in \"follow\" mode ", redirUrl, locationUrl, statusCode, "follow");
-  redirectMode("Redirect " + statusCode + " in \"manual\" mode ", redirUrl, locationUrl, statusCode, "manual");
-}
-
-done();
diff --git a/fetch/api/redirect/redirect-origin-worker.html b/fetch/api/redirect/redirect-origin-worker.html
deleted file mode 100644
index fdb5422..0000000
--- a/fetch/api/redirect/redirect-origin-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: redirect mode handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-origin.js"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-origin.any.js b/fetch/api/redirect/redirect-origin.any.js
new file mode 100644
index 0000000..3edb1bd
--- /dev/null
+++ b/fetch/api/redirect/redirect-origin.any.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testOriginAfterRedirection(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) {
+    var uuid_token = token();
+    var url = redirectUrl;
+    var urlParameters = "?token=" + uuid_token + "&max_age=0";
+    urlParameters += "&redirect_status=" + redirectStatus;
+    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+    var requestInit = {"mode": "cors", "redirect": "follow"};
+
+    promise_test(function(test) {
+        return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+            assert_equals(resp.status, 200, "Clean stash response's status is 200");
+            return fetch(url + urlParameters, requestInit).then(function(response) {
+                assert_equals(response.status, 200, "Inspect header response's status is 200");
+                assert_equals(response.headers.get("x-request-origin"), expectedOrigin, "Check origin header");
+            });
+        });
+    }, desc);
+}
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var locationUrl =  get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=origin";
+var corsLocationUrl =  get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=origin";
+
+for (var code of [301, 302, 303, 307, 308]) {
+    testOriginAfterRedirection("Same origin to same origin redirection " + code, redirectUrl, locationUrl, code, null);
+    testOriginAfterRedirection("Same origin to other origin redirection " + code, redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN);
+    testOriginAfterRedirection("Other origin to other origin redirection " + code, corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN);
+    testOriginAfterRedirection("Other origin to same origin redirection " + code, corsRedirectUrl, locationUrl + "&cors", code, "null");
+}
+
+done();
diff --git a/fetch/api/redirect/redirect-origin.html b/fetch/api/redirect/redirect-origin.html
deleted file mode 100644
index 4cbe1c0..0000000
--- a/fetch/api/redirect/redirect-origin.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: redirect mode handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="/common/utils.js"></script>
-    <script src="../resources/utils.js"></script>
-    <script src="/common/get-host-info.sub.js"></script>
-    <script src="redirect-origin.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-origin.js b/fetch/api/redirect/redirect-origin.js
deleted file mode 100644
index 77f841e..0000000
--- a/fetch/api/redirect/redirect-origin.js
+++ /dev/null
@@ -1,40 +0,0 @@
-if (this.document === undefined) {
-    importScripts("/common/utils.js");
-    importScripts("/resources/testharness.js");
-    importScripts("../resources/utils.js");
-    importScripts("/common/get-host-info.sub.js");
-}
-
-function testOriginAfterRedirection(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) {
-    var uuid_token = token();
-    var url = redirectUrl;
-    var urlParameters = "?token=" + uuid_token + "&max_age=0";
-    urlParameters += "&redirect_status=" + redirectStatus;
-    urlParameters += "&location=" + encodeURIComponent(redirectLocation);
-
-    var requestInit = {"mode": "cors", "redirect": "follow"};
-
-    promise_test(function(test) {
-        return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
-            assert_equals(resp.status, 200, "Clean stash response's status is 200");
-            return fetch(url + urlParameters, requestInit).then(function(response) {
-                assert_equals(response.status, 200, "Inspect header response's status is 200");
-                assert_equals(response.headers.get("x-request-origin"), expectedOrigin, "Check origin header");
-            });
-        });
-    }, desc);
-}
-
-var redirectUrl = RESOURCES_DIR + "redirect.py";
-var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
-var locationUrl =  get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=origin";
-var corsLocationUrl =  get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=origin";
-
-for (var code of [301, 302, 303, 307, 308]) {
-    testOriginAfterRedirection("Same origin to same origin redirection " + code, redirectUrl, locationUrl, code, null);
-    testOriginAfterRedirection("Same origin to other origin redirection " + code, redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN);
-    testOriginAfterRedirection("Other origin to other origin redirection " + code, corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN);
-    testOriginAfterRedirection("Other origin to same origin redirection " + code, corsRedirectUrl, locationUrl + "&cors", code, "null");
-}
-
-done();
diff --git a/fetch/api/redirect/redirect-referrer-worker.html b/fetch/api/redirect/redirect-referrer-worker.html
deleted file mode 100644
index 6e5cd45..0000000
--- a/fetch/api/redirect/redirect-referrer-worker.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: redirect referrer handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-redirect-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-referrer.js"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-referrer.any.js b/fetch/api/redirect/redirect-referrer.any.js
new file mode 100644
index 0000000..92f0b9b
--- /dev/null
+++ b/fetch/api/redirect/redirect-referrer.any.js
@@ -0,0 +1,65 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+  var url = redirectUrl;
+  var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+
+  if (redirectReferrerPolicy)
+    urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+  var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+
+    promise_test(function(test) {
+      return fetch(url + urlParameters, requestInit).then(function(response) {
+        assert_equals(response.status, 200, "Inspect header response's status is 200");
+        assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+      });
+    }, desc);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl =  get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl);
+
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl);
+
+testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin);
+
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin);
+
+done();
diff --git a/fetch/api/redirect/redirect-referrer.html b/fetch/api/redirect/redirect-referrer.html
deleted file mode 100644
index 473b5e6..0000000
--- a/fetch/api/redirect/redirect-referrer.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: redirect referrer handling</title>
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
-    <meta name="help" href="https://fetch.spec.whatwg.org/#http-redirect-fetch">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="/common/utils.js"></script>
-    <script src="../resources/utils.js"></script>
-    <script src="/common/get-host-info.sub.js"></script>
-    <script src="redirect-referrer.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-referrer.js b/fetch/api/redirect/redirect-referrer.js
deleted file mode 100644
index fabecea..0000000
--- a/fetch/api/redirect/redirect-referrer.js
+++ /dev/null
@@ -1,68 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/common/utils.js");
-  importScripts("/resources/testharness.js");
-  importScripts("../resources/utils.js");
-  importScripts("/common/get-host-info.sub.js");
-}
-
-function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
-  var url = redirectUrl;
-  var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
-
-  if (redirectReferrerPolicy)
-    urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
-
-  var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
-
-    promise_test(function(test) {
-      return fetch(url + urlParameters, requestInit).then(function(response) {
-        assert_equals(response.status, 200, "Inspect header response's status is 200");
-        assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
-      });
-    }, desc);
-}
-
-var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
-var referrerUrl = location.href;
-
-var redirectUrl = RESOURCES_DIR + "redirect.py";
-var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
-var crossLocationUrl =  get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
-
-testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin);
-testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null);
-testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin);
-testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl);
-
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin);
-testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl);
-
-testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl);
-testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl);
-testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null);
-testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null);
-testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin);
-
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin);
-testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin);
-
-done();
diff --git a/fetch/api/redirect/redirect-to-dataurl-worker.html b/fetch/api/redirect/redirect-to-dataurl-worker.html
deleted file mode 100644
index 428f513..0000000
--- a/fetch/api/redirect/redirect-to-dataurl-worker.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch in worker: data URL loading after redirections</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script>
-      fetch_tests_from_worker(new Worker("redirect-to-dataurl.js"));
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-to-dataurl.any.js b/fetch/api/redirect/redirect-to-dataurl.any.js
new file mode 100644
index 0000000..68ff2c5
--- /dev/null
+++ b/fetch/api/redirect/redirect-to-dataurl.any.js
@@ -0,0 +1,27 @@
+// META: script=/common/get-host-info.sub.js
+
+var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5";
+var body = "response's body";
+var contentType = "text/plain";
+
+function redirectDataURL(desc, redirectUrl, mode) {
+    var url = redirectUrl +  "?cors&location=" + encodeURIComponent(dataURL);
+
+    var requestInit = {"mode": mode};
+
+    promise_test(function(test) {
+        return promise_rejects(test, new TypeError(), fetch(url, requestInit));
+    }, desc);
+}
+
+var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
+var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py";
+
+redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin");
+
+redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors");
+redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors");
+
+done();
diff --git a/fetch/api/redirect/redirect-to-dataurl.html b/fetch/api/redirect/redirect-to-dataurl.html
deleted file mode 100644
index ed7159f..0000000
--- a/fetch/api/redirect/redirect-to-dataurl.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Fetch: data URL loading after redirections</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <script src="/common/get-host-info.sub.js"></script>
-    <script src="redirect-to-dataurl.js"></script>
-  </body>
-</html>
diff --git a/fetch/api/redirect/redirect-to-dataurl.js b/fetch/api/redirect/redirect-to-dataurl.js
deleted file mode 100644
index c3bae3f..0000000
--- a/fetch/api/redirect/redirect-to-dataurl.js
+++ /dev/null
@@ -1,30 +0,0 @@
-if (this.document === undefined) {
-  importScripts("/common/get-host-info.sub.js")
-  importScripts("/resources/testharness.js");
-}
-
-var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5";
-var body = "response's body";
-var contentType = "text/plain";
-
-function redirectDataURL(desc, redirectUrl, mode) {
-    var url = redirectUrl +  "?cors&location=" + encodeURIComponent(dataURL);
-
-    var requestInit = {"mode": mode};
-
-    promise_test(function(test) {
-        return promise_rejects(test, new TypeError(), fetch(url, requestInit));
-    }, desc);
-}
-
-var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
-var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py";
-
-redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors");
-redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors");
-redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin");
-
-redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors");
-redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors");
-
-done();
diff --git a/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/fetch/api/request/destination/fetch-destination-no-load-event.https.html
index 5be882f..1778bf2 100644
--- a/fetch/api/request/destination/fetch-destination-no-load-event.https.html
+++ b/fetch/api/request/destination/fetch-destination-no-load-event.https.html
@@ -9,7 +9,8 @@
 
 // Set up the service worker and the frame.
 promise_test(t => {
-    const kScope = 'resources/empty.https.html';
+    const kScope = 'resources/';
+    const kFrame = 'resources/empty.https.html';
     const kScript = 'resources/fetch-destination-worker-no-load-event.js';
     return service_worker_unregister_and_register(t, kScript, kScope)
       .then(registration => {
@@ -20,7 +21,7 @@
           return wait_for_state(t, registration.installing, 'activated');
         })
       .then(() => {
-          return with_iframe(kScope);
+          return with_iframe(kFrame);
         })
       .then(f => {
           frame = f;
@@ -111,7 +112,6 @@
 
 // style destination
 /////////////////////
-
 // @import - style destination
 promise_test(async t => {
     let node = frame.contentWindow.document.createElement("style");
diff --git a/fetch/api/request/destination/fetch-destination.https.html b/fetch/api/request/destination/fetch-destination.https.html
index 27b5102..5d9f717 100644
--- a/fetch/api/request/destination/fetch-destination.https.html
+++ b/fetch/api/request/destination/fetch-destination.https.html
@@ -425,4 +425,18 @@
   });
 }, 'HTMLLinkElement with rel=preload and as=manifest fetches with a "manifest" Request.destination');
 
+// HTMLLinkElement with rel=prefetch - empty string destination
+promise_test(async t => {
+  await new Promise((resolve, reject) => {
+      let node = frame.contentWindow.document.createElement("link");
+      node.rel = "prefetch";
+      node.onload = resolve;
+      node.onerror = reject;
+      node.href = "dummy?dest=";
+      frame.contentWindow.document.body.appendChild(node);
+  }).catch(() => {
+      assert_unreached("Fetch errored.");
+  });
+}, 'HTMLLinkElement with rel=prefetch fetches with an empty string Request.destination');
+
 </script>
diff --git a/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
index fd71ffb..5a3c679 100644
--- a/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
+++ b/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
@@ -2,11 +2,13 @@
     if (event.request.url.includes('dummy')) {
         event.waitUntil(async function() {
             let destination = new URL(event.request.url).searchParams.get("dest");
-            let client = await self.clients.get(event.clientId);
+            var result = "FAIL";
             if (event.request.destination == destination) {
-                client.postMessage("PASS");
-            } else {
-                client.postMessage("FAIL");
+              result = "PASS";
+            }
+            let cl = await clients.matchAll({includeUncontrolled: true});
+            for (i = 0; i < cl.length; i++) {
+              cl[i].postMessage(result);
             }
         }())
     }
diff --git a/fetch/api/request/request-idl.html b/fetch/api/request/request-idl.html
deleted file mode 100644
index f783377..0000000
--- a/fetch/api/request/request-idl.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Request idl interface</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#response">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <script src="/resources/WebIDLParser.js"></script>
-    <script src="/resources/idlharness.js"></script>
-  </head>
-  <body>
-    <script id="body-idl" type="text/plain">
-      typedef any JSON;
-      typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit;
-
-      interface mixin Body {
-        readonly attribute ReadableStream? body;
-        readonly attribute boolean bodyUsed;
-        [NewObject] Promise<ArrayBuffer> arrayBuffer();
-        [NewObject] Promise<Blob> blob();
-        [NewObject] Promise<FormData> formData();
-        [NewObject] Promise<JSON> json();
-        [NewObject] Promise<USVString> text();
-      };
-    </script>
-    <script id="request-idl" type="text/plain">
-      typedef (Request or USVString) RequestInfo;
-
-      [Constructor(RequestInfo input, optional RequestInit init),
-      Exposed=(Window,Worker)]
-      interface Request {
-        readonly attribute ByteString method;
-        readonly attribute USVString url;
-        [SameObject] readonly attribute Headers headers;
-
-        readonly attribute RequestDestination destination;
-        readonly attribute USVString referrer;
-        readonly attribute ReferrerPolicy referrerPolicy;
-        readonly attribute RequestMode mode;
-        readonly attribute RequestCredentials credentials;
-        readonly attribute RequestCache cache;
-        readonly attribute RequestRedirect redirect;
-        readonly attribute DOMString integrity;
-
-        [NewObject] Request clone();
-      };
-      Request includes Body;
-
-      dictionary RequestInit {
-        ByteString method;
-        HeadersInit headers;
-        BodyInit? body;
-        USVString referrer;
-        ReferrerPolicy referrerPolicy;
-        RequestMode mode;
-        RequestCredentials credentials;
-        RequestCache cache;
-        RequestRedirect redirect;
-        DOMString integrity;
-        any window; // can only be set to null
-      };
-
-      enum RequestType { "", "audio", "font", "image", "script", "style", "track", "video" };
-      enum RequestDestination { "", "document", "sharedworker", "subresource", "unknown", "worker" };
-      enum RequestMode { "navigate", "same-origin", "no-cors", "cors" };
-      enum RequestCredentials { "omit", "same-origin", "include" };
-      enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" };
-      enum RequestRedirect { "follow", "error", "manual" };
-      enum ReferrerPolicy {
-        "", "no-referrer", "no-referrer-when-downgrade", "origin",
-        "origin-when-cross-origin", "unsafe-url", "same-origin", "strict-origin",
-        "strict-origin-when-cross-origin"
-      };
-    </script>
-    <script>
-      var idlsArray = new IdlArray();
-      var idl = document.getElementById("body-idl").textContent
-      idl += document.getElementById("request-idl").textContent
-
-      idlsArray.add_idls(idl);
-      idlsArray.add_untested_idls("interface Headers {};");
-      idlsArray.add_objects({ Request: ['new Request("")'] });
-      idlsArray.test();
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/request/request-init-002.html b/fetch/api/request/request-init-002.html
index 5d92b09..221b415 100644
--- a/fetch/api/request/request-init-002.html
+++ b/fetch/api/request/request-init-002.html
@@ -43,7 +43,7 @@
             //not equals: cannot guess formData exact value
             assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body");
           });
-        }, "Initialize Request's body with " + bodyType);
+        }, `Initialize Request's body with "${body}", ${bodyType}`);
       }
 
       var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
@@ -56,6 +56,7 @@
       checkRequestInit(blob, "application/octet-binary", "This is a blob");
       checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
       checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+      checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!");
 
       // Ensure test does not time out in case of missing URLSearchParams support.
       if (window.URLSearchParams) {
diff --git a/fetch/api/request/request-init-003.sub.html b/fetch/api/request/request-init-003.sub.html
index 507007f..79c91cd 100644
--- a/fetch/api/request/request-init-003.sub.html
+++ b/fetch/api/request/request-init-003.sub.html
@@ -44,7 +44,7 @@
                              "referrer" : "about:client",
                              "referrerPolicy" : "",
                              "mode" : "cors",
-                             "credentials" : "omit",
+                             "credentials" : "same-origin",
                              "cache" : "default",
                              "redirect" : "follow",
                              "integrity" : "",
diff --git a/fetch/api/request/request-reset-attributes.https.html b/fetch/api/request/request-reset-attributes.https.html
new file mode 100644
index 0000000..ba04d19
--- /dev/null
+++ b/fetch/api/request/request-reset-attributes.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/request-reset-attributes-worker.js';
+
+promise_test(async (t) => {
+    const scope = 'resources/hello.txt?name=isReloadNavigation';
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'old: false, new: false');
+      await new Promise((resolve) => {
+        frame.onload = resolve;
+        frame.contentWindow.location.reload();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'old: true, new: false');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+  }, 'Request.isReloadNavigation is reset with non-empty RequestInit');
+</script>
diff --git a/fetch/api/request/request-structure.html b/fetch/api/request/request-structure.html
index 98c2451..3f82e9c 100644
--- a/fetch/api/request/request-structure.html
+++ b/fetch/api/request/request-structure.html
@@ -76,7 +76,7 @@
             break;
 
           case "credentials":
-            defaultValue = "omit";
+            defaultValue = "same-origin";
             newValue = "cors";
             break;
 
@@ -99,6 +99,11 @@
             newValue = true;
             break;
 
+          case "isReloadNavigation":
+            defaultValue = false;
+            newValue = true;
+            break;
+
           default:
             return;
         }
diff --git a/fetch/api/request/resources/hello.txt b/fetch/api/request/resources/hello.txt
new file mode 100644
index 0000000..ce01362
--- /dev/null
+++ b/fetch/api/request/resources/hello.txt
@@ -0,0 +1 @@
+hello
diff --git a/fetch/api/request/resources/request-reset-attributes-worker.js b/fetch/api/request/resources/request-reset-attributes-worker.js
new file mode 100644
index 0000000..4b264ca
--- /dev/null
+++ b/fetch/api/request/resources/request-reset-attributes-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('fetch', (event) => {
+    const params = new URL(event.request.url).searchParams;
+    if (params.has('ignore')) {
+      return;
+    }
+    if (!params.has('name')) {
+      event.respondWith(Promise.reject(TypeError('No name is provided.')));
+      return;
+    }
+
+    const name = params.get('name');
+    const old_attribute = event.request[name];
+    // If any of |init|'s member is present...
+    const init = {cache: 'no-store'}
+    const new_attribute = (new Request(event.request, init))[name];
+
+    event.respondWith(
+      new Response(`old: ${old_attribute}, new: ${new_attribute}`));
+  });
diff --git a/fetch/api/resources/cors-top.txt b/fetch/api/resources/cors-top.txt
new file mode 100644
index 0000000..83a3157
--- /dev/null
+++ b/fetch/api/resources/cors-top.txt
@@ -0,0 +1 @@
+top
\ No newline at end of file
diff --git a/fetch/api/resources/cors-top.txt.headers b/fetch/api/resources/cors-top.txt.headers
new file mode 100644
index 0000000..cb762ef
--- /dev/null
+++ b/fetch/api/resources/cors-top.txt.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/fetch/api/response/response-idl.html b/fetch/api/response/response-idl.html
deleted file mode 100644
index bd265fa..0000000
--- a/fetch/api/response/response-idl.html
+++ /dev/null
@@ -1,68 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Response idl interface</title>
-    <meta name="help" href="https://fetch.spec.whatwg.org/#response">
-    <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <script src="/resources/WebIDLParser.js"></script>
-    <script src="/resources/idlharness.js"></script>
-  </head>
-  <body>
-    <script id="body-idl" type="text/plain">
-      typedef any JSON;
-      typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit;
-
-      interface mixin Body {
-        readonly attribute ReadableStream? body;
-        readonly attribute boolean bodyUsed;
-        [NewObject] Promise<ArrayBuffer> arrayBuffer();
-        [NewObject] Promise<Blob> blob();
-        [NewObject] Promise<FormData> formData();
-        [NewObject] Promise<JSON> json();
-        [NewObject] Promise<USVString> text();
-      };
-    </script>
-    <script id="response-idl" type="text/plain">
-      [Constructor(optional BodyInit body, optional ResponseInit init),
-      Exposed=(Window,Worker)]
-      interface Response {
-        [NewObject] static Response error();
-        [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
-
-        readonly attribute ResponseType type;
-
-        readonly attribute USVString url;
-        readonly attribute unsigned short status;
-        readonly attribute boolean ok;
-        readonly attribute ByteString statusText;
-        [SameObject] readonly attribute Headers headers;
-        readonly attribute Promise<Headers> trailer;
-
-        [NewObject] Response clone();
-      };
-      Response includes Body;
-
-      dictionary ResponseInit {
-        unsigned short status = 200;
-        ByteString statusText = "OK";
-        HeadersInit headers;
-      };
-
-      enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" };
-    </script>
-    <script>
-      var idlsArray = new IdlArray();
-      var idl = document.getElementById("body-idl").textContent
-      idl += document.getElementById("response-idl").textContent
-
-      idlsArray.add_idls(idl);
-      idlsArray.add_untested_idls("interface Headers {};");
-      idlsArray.add_untested_idls("interface ReadableStream {};");
-      idlsArray.add_objects({ Response: ['new Response()'] });
-      idlsArray.test();
-    </script>
-  </body>
-</html>
diff --git a/fetch/api/response/response-static-redirect.html b/fetch/api/response/response-static-redirect.html
index e09c666..a749222 100644
--- a/fetch/api/response/response-static-redirect.html
+++ b/fetch/api/response/response-static-redirect.html
@@ -14,16 +14,24 @@
       var url = "http://test.url:1234/";
       test(function() {
         redirectResponse = Response.redirect(url);
+        assert_equals(redirectResponse.type, "default");
+        assert_false(redirectResponse.redirected);
+        assert_false(redirectResponse.ok);
         assert_equals(redirectResponse.status, 302, "Default redirect status is 302");
         assert_equals(redirectResponse.headers.get("Location"), url,
           "redirected response has Location header with the correct url");
+        assert_equals(redirectResponse.statusText, "");
       }, "Check default redirect response");
 
-      var redirectStatus = [301, 302, 303, 307, 308];
-      redirectStatus.forEach(function(status) {
+      [301, 302, 303, 307, 308].forEach(function(status) {
         test(function() {
           redirectResponse = Response.redirect(url, status);
+          assert_equals(redirectResponse.type, "default");
+          assert_false(redirectResponse.redirected);
+          assert_false(redirectResponse.ok);
           assert_equals(redirectResponse.status, status, "Redirect status is " + status);
+          assert_equals(redirectResponse.headers.get("Location"), url);
+          assert_equals(redirectResponse.statusText, "");
         }, "Check response returned by static method redirect(), status = " + status);
       });
 
diff --git a/fetch/content-encoding/bad-gzip-body.any.js b/fetch/content-encoding/bad-gzip-body.any.js
new file mode 100644
index 0000000..34557de
--- /dev/null
+++ b/fetch/content-encoding/bad-gzip-body.any.js
@@ -0,0 +1,20 @@
+promise_test((test) => {
+    return fetch("resources/bad-gzip-body.py").then(res => {
+      assert_equals(res.status, 200);
+    });
+}, "Fetching a resource with bad gzip content should still resolve");
+
+[
+  "arrayBuffer",
+  "blob",
+  "formData",
+  "json",
+  "text"
+].forEach(method => {
+  promise_test(t => {
+    return fetch("resources/bad-gzip-body.py").then(res => {
+      assert_equals(res.status, 200);
+      return promise_rejects(t, new TypeError(), res[method]());
+    });
+  }, "Consuming the body of a resource with bad gzip content with " + method + "() should reject");
+});
diff --git a/fetch/content-encoding/resources/bad-gzip-body.py b/fetch/content-encoding/resources/bad-gzip-body.py
new file mode 100644
index 0000000..b2f8cfe
--- /dev/null
+++ b/fetch/content-encoding/resources/bad-gzip-body.py
@@ -0,0 +1,3 @@
+def main(request, response):
+    headers = [("Content-Encoding", "gzip")]
+    return headers, "not actually gzip"
diff --git a/fetch/corb/README.md b/fetch/corb/README.md
index f16943e..04c5b8a 100644
--- a/fetch/corb/README.md
+++ b/fetch/corb/README.md
@@ -26,18 +26,8 @@
 is included in the official
 [Fetch spec](https://fetch.spec.whatwg.org/).  Such tests may fail unless
 CORB is enabled.  In practice this means that:
-* Such tests will fail in default Chromium and have to be listed
-  in `third_party/WebKit/LayoutTests/TestExpectations` and associated
-  with https://crbug.com/802835.
-* Such tests will pass in Chromium when either
-  1) CORB is explicitly, manually enabled by passing extra cmdline flags to
-     `run-webkit-tests`:
-     `--additional-driver-flag=--enable-features=CrossSiteDocumentBlockingAlways` and
-     `--additional-expectations=third_party/WebKit/LayoutTests/FlagExpectations/site-per-process`.
-  2) CORB is implicitly enabled via Site Isolation (e.g. in
-     `site_per_process_webkit_layout_tests` step on the test bots).  The
-     expectations that the tests pass in this mode is controlled by the
-     `third_party/WebKit/LayoutTests/FlagExpectations/site-per-process` file.
+* Such tests will pass in Chromium
+  (where CORB is enabled by default [since M68](https://crrev.com/553830)).
 * Such tests may fail in other browsers.
 
 
diff --git a/fetch/corb/img-mime-types-coverage.tentative.sub.html b/fetch/corb/img-mime-types-coverage.tentative.sub.html
index 4ed8f01..7ccc41b 100644
--- a/fetch/corb/img-mime-types-coverage.tentative.sub.html
+++ b/fetch/corb/img-mime-types-coverage.tentative.sub.html
@@ -28,7 +28,7 @@
       // - https://tools.ietf.org/html/rfc7303
       "text/x-json", "text/json+blah", "application/json+blah",
       "text/xml+blah", "application/xml+blah",
-      "application/blahjson", "text/blahxml",
+      "application/blahjson", "text/blahxml"]
 
   var fails = [
       // CORB-protected MIME-types - i.e. ones covered by:
diff --git a/fetch/data-urls/resources/base64.json b/fetch/data-urls/resources/base64.json
index 7de20b2..c4f79aa 100644
--- a/fetch/data-urls/resources/base64.json
+++ b/fetch/data-urls/resources/base64.json
@@ -49,6 +49,7 @@
   ["ab=c=", null],
   ["abc=d", null],
   ["abc=d=", null],
+  ["ab\u000Bcd", null],
   ["ab\tcd", [105, 183, 29]],
   ["ab\ncd", [105, 183, 29]],
   ["ab\fcd", [105, 183, 29]],
diff --git a/fonts/README.md b/fonts/README.md
index f69b95a..7c6ed9a 100644
--- a/fonts/README.md
+++ b/fonts/README.md
@@ -1,2 +1,6 @@
 This directory only contains auxiliary font files used by other tests. See
 /css-fonts for tests covering the CSS Fonts Module specification.
+
+The font named `Ahem.ttf` is referenced from the project documentation and the
+CLI's scripts for provisioning virtual machines provided by Sauce Labs. If that
+file is re-located, the references should be updated accordingly.
diff --git a/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html b/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html
index f684bcb..b22fbed 100644
--- a/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html
+++ b/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html
@@ -57,7 +57,7 @@
     // we automatically assume navigation succeeded after 100 ms. A sibling test will test this
     // particular Firefox bug.
     await navigateAndWaitForChange(frameWindow, w => w.history.forward(),
-      { assumeSuccessAfter: 100 });
+      { assumeSuccessAfter: 500 });
 
     assert_equals(frameWindow.location.hash, "#cat");
     assert_equals(frameWindow.history.length, afterThreeNavigations,
diff --git a/html/browsers/browsing-the-web/unloading-documents/unload/007.html b/html/browsers/browsing-the-web/unloading-documents/unload/007.html
index 0d5b72e..4a2fed5 100644
--- a/html/browsers/browsing-the-web/unloading-documents/unload/007.html
+++ b/html/browsers/browsing-the-web/unloading-documents/unload/007.html
@@ -4,17 +4,17 @@
 <script src="/resources/testharnessreport.js"></script>
 <div id="log"></div>
 <script>
-var t = async_test(undefined, {timeout:2000});
+var t = async_test();
 
 var loaded = false;
 var unload_fired = false;
 var timeout_fired = false;
 
 function start_test() {
-  setTimeout(t.step_func(function() {
-                           assert_true(unload_fired);
-                           assert_false(timeout_fired);
-                           t.done()
+  step_timeout(t.step_func(function() {
+                             assert_true(unload_fired);
+                             assert_false(timeout_fired);
+                             t.done()
                          }), 1000);
 }
 
diff --git a/html/browsers/history/joint-session-history/joint-session-history-filler.html b/html/browsers/history/joint-session-history/joint-session-history-filler.html
new file mode 100644
index 0000000..b6d47c3
--- /dev/null
+++ b/html/browsers/history/joint-session-history/joint-session-history-filler.html
@@ -0,0 +1 @@
+<body>Filler</body>
diff --git a/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html b/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html
new file mode 100644
index 0000000..b66adcb
--- /dev/null
+++ b/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Joint session history length does not include entries from a removed iframe.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<iframe id="frame" src="about:blank"></iframe>
+<script>
+async_test(function(t) {
+    t.step_timeout(() => {
+        var child = document.getElementById("frame");
+        var old_history_len = history.length;
+        child.onload = () => {
+            assert_equals(old_history_len + 1, history.length);
+            document.body.removeChild(document.getElementById("frame"));
+            assert_equals(old_history_len, history.length);
+            t.done();
+        }
+        child.src = "joint-session-history-filler.html";
+    }, 1000);
+});
+</script>
+</body>
diff --git a/html/browsers/history/the-history-interface/history_pushstate_url.html b/html/browsers/history/the-history-interface/history_pushstate_url.html
new file mode 100644
index 0000000..cfbca35
--- /dev/null
+++ b/html/browsers/history/the-history-interface/history_pushstate_url.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>History pushState sets the url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+async_test(function(t) {
+    var oldLocation = window.location.toString();
+    window.history.pushState(null, "", "#hash");
+    assert_equals(oldLocation + "#hash", window.location.toString(), "pushState updates url");
+    history.back();
+    window.onhashchange = () => {
+        assert_equals(oldLocation, window.location.toString(), 'history traversal restores old url');
+        t.done();
+    };
+}, "history pushState sets url");
+</script>
+</body>
+</html>
diff --git a/html/browsers/offline/browser-state/navigator_online_event-manual.https.html b/html/browsers/offline/browser-state/navigator_online_event-manual.https.html
index b100f17..81cad4f 100644
--- a/html/browsers/offline/browser-state/navigator_online_event-manual.https.html
+++ b/html/browsers/offline/browser-state/navigator_online_event-manual.https.html
@@ -25,12 +25,18 @@
   </div>
   <script>
 
-  function showOnline() {
-    document.getElementById('actualMsg').innerHTML = 'online event is raised.';
+  function showOnline(e) {
+    let msg = 'online event is raised';
+    if (e.target != window)
+      msg += ' (on the WRONG target)';
+    document.getElementById('actualMsg').innerHTML = msg + '.';
   }
 
-  function showOffline() {
-    document.getElementById('actualMsg').innerHTML = 'offline event is raised.';
+  function showOffline(e) {
+    let msg = 'offline event is raised';
+    if (e.target != window)
+      msg += ' (on the WRONG target)';
+    document.getElementById('actualMsg').innerHTML = msg + '.';
   }
 
   window.addEventListener("online", showOnline, false);
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/.gitkeep b/html/browsers/origin/relaxing-the-same-origin-restriction/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/html/browsers/origin/relaxing-the-same-origin-restriction/.gitkeep
+++ /dev/null
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html b/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
index bf5b0bc..6ffea4f 100644
--- a/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
+++ b/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
@@ -15,7 +15,7 @@
       var SUFFIX_HOST = ORIGINAL_HOST.substring(ORIGINAL_HOST.lastIndexOf('.') + 1); // e.g. "test"
       var PREFIX_HOST = "www1." + ORIGINAL_HOST; // e.g. "www1.web-platform.test"
       var iframe = document.getElementById("iframe");
-      var iframe_url = new URL("document_domain_setter_iframe.html", document.location);
+      var iframe_url = new URL("support/document_domain_setter_iframe.html", document.location);
       iframe_url.hostname = PREFIX_HOST;
       iframe.src = iframe_url;
       test(function() {
@@ -25,11 +25,11 @@
         assert_throws("SecurityError", function() { document.domain = "example.com"; });
       }, "failed setting of document.domain");
       async_test(function(t) {
-        iframe.addEventListener("load", t.step_func(function() {
+        iframe.addEventListener("load", t.step_func_done(function() {
           // Before setting document.domain, the iframe is not
           // same-origin-domain, so security checks fail.
           assert_equals(iframe.contentDocument, null);
-          assert_equals(iframe.contentWindow.frameElement, null);
+          assert_throws("SecurityError", () => iframe.contentWindow.frameElement);
           assert_throws("SecurityError", function() { iframe.contentWindow.location.origin; });
           assert_throws("SecurityError", function() { iframe.contentWindow.location.href; });
           assert_throws("SecurityError", function() { iframe.contentWindow.location.protocol; });
@@ -45,8 +45,8 @@
           // After setting document.domain, the iframe is
           // same-origin-domain, so security checks pass.
           assert_equals(iframe.contentDocument.domain, document.domain);
-          assert_equals(iframe.contentWindow.frameElement, iframe)
-          assert_equals(iframe.contentWindow.origin, window.origin);
+          assert_equals(iframe.contentWindow.frameElement, iframe);
+          assert_equals(iframe.contentWindow.origin, iframe_url.origin);
           assert_equals(iframe.contentWindow.location.href, iframe_url.href);
           assert_equals(iframe.contentWindow.location.protocol, iframe_url.protocol);
           assert_equals(iframe.contentWindow.location.host, iframe_url.host);
@@ -60,7 +60,6 @@
           // document.open checks for same-origin, not same-origin-domain,
           // https://github.com/whatwg/html/issues/2282
           assert_throws("SecurityError", function() { iframe.contentDocument.open(); });
-          t.done();
         }));
       }, "same-origin-domain iframe");
     </script>
diff --git a/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_iframe.html b/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html
similarity index 100%
rename from html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_iframe.html
rename to html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html
diff --git a/html/dom/dynamic-markup-insertion/opening-the-input-stream/encoding.window.js b/html/dom/dynamic-markup-insertion/opening-the-input-stream/encoding.window.js
new file mode 100644
index 0000000..a610ce9
--- /dev/null
+++ b/html/dom/dynamic-markup-insertion/opening-the-input-stream/encoding.window.js
@@ -0,0 +1,12 @@
+async_test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe"));
+  frame.src = "resources/encoding-frame.html";
+  frame.onload = t.step_func_done(t => {
+    // Using toLowerCase() to avoid an Edge bug
+    assert_equals(frame.contentDocument.characterSet.toLowerCase(), "shift_jis", "precondition");
+    frame.contentDocument.open();
+    assert_equals(frame.contentDocument.characterSet.toLowerCase(), "shift_jis", "actual test");
+    frame.contentDocument.close();
+    assert_equals(frame.contentDocument.characterSet.toLowerCase(), "shift_jis", "might as well");
+  });
+}, "doucment.open() and the document's encoding");
diff --git a/html/dom/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js b/html/dom/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js
new file mode 100644
index 0000000..97334ce
--- /dev/null
+++ b/html/dom/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js
@@ -0,0 +1,42 @@
+test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe")),
+        body = frame.contentDocument.body;
+  t.add_cleanup(() => frame.remove());
+  frame.contentDocument.addEventListener("x", t.unreached_func("document event listener not removed"));
+  body.addEventListener("x", t.unreached_func("body event listener not removed"));
+  frame.contentDocument.open();
+  frame.contentDocument.dispatchEvent(new Event("x"));
+  body.dispatchEvent(new Event("x"));
+  frame.contentDocument.close();
+}, "Event listeners are to be removed");
+
+test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe"));
+  t.add_cleanup(() => frame.remove());
+  let once = false;
+  frame.contentDocument.addEventListener("x", () => {
+    frame.contentDocument.open();
+    once = true;
+  });
+  frame.contentDocument.addEventListener("x", t.unreached_func("second event listener not removed"));
+  frame.contentDocument.dispatchEvent(new Event("x"));
+  assert_true(once);
+  frame.contentDocument.close();
+}, "Event listeners are to be removed with immediate effect");
+
+test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe")),
+        shadow = frame.contentDocument.body.attachShadow({ mode: "closed" }),
+        shadowChild = shadow.appendChild(document.createElement("div")),
+        shadowShadow = shadowChild.attachShadow({ mode: "open" }),
+        nodes = [shadow, shadowChild, shadowShadow];
+  t.add_cleanup(() => frame.remove());
+  nodes.forEach(node => {
+    node.addEventListener("x", t.unreached_func(node + "'s event listener not removed"));
+  });
+  frame.contentDocument.open();
+  nodes.forEach(node => {
+    node.dispatchEvent(new Event("x"));
+  });
+  frame.contentDocument.close();
+}, "Event listeners are to be removed from shadow trees as well");
diff --git a/html/dom/dynamic-markup-insertion/opening-the-input-stream/mutation-events.window.js b/html/dom/dynamic-markup-insertion/opening-the-input-stream/mutation-events.window.js
new file mode 100644
index 0000000..5c8e7d6
--- /dev/null
+++ b/html/dom/dynamic-markup-insertion/opening-the-input-stream/mutation-events.window.js
@@ -0,0 +1,22 @@
+// In an ideal world this test would eventually be obsolete due to mutation events disappearing. Or
+// would have to change to account for mutation events not firing synchronously. Neither seems
+// realistic to the author though.
+
+test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe"));
+  frame.contentWindow.addEventListener("DOMNodeInserted", t.unreached_func());
+  frame.contentWindow.addEventListener("DOMNodeInserted", t.unreached_func(), true);
+  frame.contentWindow.addEventListener("DOMNodeInsertedIntoDocument", t.unreached_func(), true);
+  frame.contentWindow.addEventListener("DOMNodeRemoved", t.unreached_func());
+  frame.contentWindow.addEventListener("DOMNodeRemoved", t.unreached_func(), true);
+  frame.contentWindow.addEventListener("DOMNodeRemovedFromDocument", t.unreached_func(), true);
+  frame.contentWindow.addEventListener("DOMSubtreeModified", t.unreached_func());
+  frame.contentWindow.addEventListener("DOMSubtreeModified", t.unreached_func(), true);
+  assert_equals(frame.contentDocument.documentElement.localName, "html");
+  frame.contentDocument.open();
+  assert_equals(frame.contentDocument.documentElement, null);
+  frame.contentDocument.write("<div>heya</div>");
+  frame.contentDocument.close();
+  assert_equals(frame.contentDocument.documentElement.localName, "html");
+  frame.remove();
+}, "document.open(), the HTML parser, and mutation events");
diff --git a/html/dom/dynamic-markup-insertion/opening-the-input-stream/resources/encoding-frame.html b/html/dom/dynamic-markup-insertion/opening-the-input-stream/resources/encoding-frame.html
new file mode 100644
index 0000000..843c3a2
--- /dev/null
+++ b/html/dom/dynamic-markup-insertion/opening-the-input-stream/resources/encoding-frame.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<meta charset=ms932>
+<p>Encoded in Shift_JIS.</p>
diff --git a/html/dom/elements/the-innertext-idl-attribute/OWNERS b/html/dom/elements/the-innertext-idl-attribute/OWNERS
new file mode 100644
index 0000000..6385161
--- /dev/null
+++ b/html/dom/elements/the-innertext-idl-attribute/OWNERS
@@ -0,0 +1 @@
+@zcorpan
diff --git a/html/dom/elements/the-innertext-idl-attribute/getter-tests.js b/html/dom/elements/the-innertext-idl-attribute/getter-tests.js
new file mode 100644
index 0000000..77a3ebb
--- /dev/null
+++ b/html/dom/elements/the-innertext-idl-attribute/getter-tests.js
@@ -0,0 +1,350 @@
+testText("<div>abc", "abc", "Simplest possible test");
+
+/**** white-space:normal ****/
+
+testText("<div> abc", "abc", "Leading whitespace removed");
+testText("<div>abc ", "abc", "Trailing whitespace removed");
+testText("<div>abc  def", "abc def", "Internal whitespace compressed");
+testText("<div>abc\ndef", "abc def", "\\n converted to space");
+testText("<div>abc\rdef", "abc def", "\\r converted to space");
+testText("<div>abc\tdef", "abc def", "\\t converted to space");
+testText("<div>abc <br>def", "abc\ndef", "Trailing whitespace before hard line break removed");
+
+/**** <pre> ****/
+
+testText("<pre> abc", " abc", "Leading whitespace preserved");
+testText("<pre>abc ", "abc ", "Trailing whitespace preserved");
+testText("<pre>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<pre>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<pre>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<pre>abc\tdef", "abc\tdef", "\\t preserved");
+testText("<div><pre>abc</pre><pre>def</pre>", "abc\ndef", "Two <pre> siblings");
+
+/**** <div style="white-space:pre"> ****/
+
+testText("<div style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
+testText("<div style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
+testText("<div style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<div style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<div style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<div style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
+
+/**** <span style="white-space:pre"> ****/
+
+testText("<span style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
+testText("<span style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
+testText("<span style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
+testText("<span style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<span style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<span style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
+
+/**** <div style="white-space:pre-line"> ****/
+
+testText("<div style='white-space:pre-line'> abc", "abc", "Leading whitespace removed");
+testText("<div style='white-space:pre-line'>abc ", "abc", "Trailing whitespace removed");
+testText("<div style='white-space:pre-line'>abc  def", "abc def", "Internal whitespace collapsed");
+testText("<div style='white-space:pre-line'>abc\ndef", "abc\ndef", "\\n preserved");
+testText("<div style='white-space:pre-line'>abc\rdef", "abc\ndef", "\\r converted to newline");
+testText("<div style='white-space:pre-line'>abc\tdef", "abc def", "\\t converted to space");
+
+/**** Collapsing whitespace across element boundaries ****/
+
+testText("<div><span>abc </span> def", "abc def", "Whitespace collapses across element boundaries");
+testText("<div><span>abc </span><span></span> def", "abc def", "Whitespace collapses across element boundaries");
+testText("<div><span>abc </span><span style='white-space:pre'></span> def", "abc def", "Whitespace collapses across element boundaries");
+
+/**** Soft line breaks ****/
+
+testText("<div style='width:0'>abc def", "abc def", "Soft line breaks ignored");
+
+/**** first-line/first-letter ****/
+
+testText("<div class='first-line-uppercase' style='width:0'>abc def", "ABC def", "::first-line styles applied");
+testText("<div class='first-letter-uppercase' style='width:0'>abc def", "Abc def", "::first-letter styles applied");
+testText("<div class='first-letter-float' style='width:0'>abc def", "abc def", "::first-letter float ignored");
+
+/**** &nbsp; ****/
+
+testText("<div>&nbsp;", "\xA0", "&nbsp; preserved");
+
+/**** display:none ****/
+
+testText("<div style='display:none'>abc", "abc", "display:none container");
+testText("<div style='display:none'>abc  def", "abc  def", "No whitespace compression in display:none container");
+testText("<div style='display:none'> abc def ", " abc def ", "No removal of leading/trailing whitespace in display:none container");
+testText("<div>123<span style='display:none'>abc", "123", "display:none child not rendered");
+testText("<div style='display:none'><span id='target'>abc", "abc", "display:none container with non-display-none target child");
+testTextInSVG("<div id='target'>abc", "", "non-display-none child of svg");
+testTextInSVG("<div style='display:none' id='target'>abc", "abc", "display:none child of svg");
+testTextInSVG("<div style='display:none'><div id='target'>abc", "abc", "child of display:none child of svg");
+
+/**** display:contents ****/
+
+if (CSS.supports("display", "contents")) {
+  testText("<div style='display:contents'>abc", "abc", "display:contents container");
+  testText("<div><div style='display:contents'>abc", "abc", "display:contents container");
+  testText("<div>123<span style='display:contents'>abc", "123abc", "display:contents rendered");
+  testText("<div style='display:contents'>   ", "", "display:contents not processed via textContent");
+  testText("<div><div style='display:contents'>   ", "", "display:contents not processed via textContent");
+}
+
+/**** visibility:hidden ****/
+
+testText("<div style='visibility:hidden'>abc", "", "visibility:hidden container");
+testText("<div>123<span style='visibility:hidden'>abc", "123", "visibility:hidden child not rendered");
+testText("<div style='visibility:hidden'>123<span style='visibility:visible'>abc", "abc", "visibility:visible child rendered");
+
+/**** visibility:collapse ****/
+
+testText("<table><tbody style='visibility:collapse'><tr><td>abc", "", "visibility:collapse row-group");
+testText("<table><tr style='visibility:collapse'><td>abc", "", "visibility:collapse row");
+testText("<table><tr><td style='visibility:collapse'>abc", "", "visibility:collapse cell");
+testText("<table><tbody style='visibility:collapse'><tr><td style='visibility:visible'>abc", "abc",
+         "visibility:collapse row-group with visible cell");
+testText("<table><tr style='visibility:collapse'><td style='visibility:visible'>abc", "abc",
+         "visibility:collapse row with visible cell");
+testText("<div style='display:flex'><span style='visibility:collapse'>1</span><span>2</span></div>",
+         "2", "visibility:collapse honored on flex item");
+testText("<div style='display:grid'><span style='visibility:collapse'>1</span><span>2</span></div>",
+         "2", "visibility:collapse honored on grid item");
+
+/**** opacity:0 ****/
+
+testText("<div style='opacity:0'>abc", "abc", "opacity:0 container");
+testText("<div style='opacity:0'>abc  def", "abc def", "Whitespace compression in opacity:0 container");
+testText("<div style='opacity:0'> abc def ", "abc def", "Remove leading/trailing whitespace in opacity:0 container");
+testText("<div>123<span style='opacity:0'>abc", "123abc", "opacity:0 child rendered");
+
+/**** generated content ****/
+
+testText("<div class='before'>", "", "Generated content not included");
+testText("<div><div class='before'>", "", "Generated content on child not included");
+
+/**** innerText on replaced elements ****/
+
+testText("<button>abc", "abc", "<button> contents preserved");
+testText("<fieldset>abc", "abc", "<fieldset> contents preserved");
+testText("<fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
+testText("<input type='text' value='abc'>", "", "<input> contents ignored");
+testText("<textarea>abc", "", "<textarea> contents ignored");
+testText("<iframe>abc", "", "<iframe> contents ignored");
+testText("<iframe><div id='target'>abc", "", "<iframe> contents ignored");
+testText("<iframe src='data:text/html,abc'>", "","<iframe> subdocument ignored");
+testText("<audio style='display:block'>abc", "", "<audio> contents ignored");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "", "<audio> contents ignored");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok if display:none");
+testText("<video>abc", "", "<video> contents ignored");
+testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "", "<video> contents ignored");
+testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok if display:none");
+testText("<canvas>abc", "", "<canvas> contents ignored");
+testText("<canvas><div id='target'>abc", "", "<canvas><div id='target'> contents ignored");
+testText("<img alt='abc'>", "", "<img> alt text ignored");
+testText("<img src='about:blank' class='poke'>", "", "<img> contents ignored");
+
+/**** <select>, <optgroup> & <option> ****/
+
+testText("<select size='1'><option>abc</option><option>def", "abc\ndef", "<select size='1'> contents of options preserved");
+testText("<select size='2'><option>abc</option><option>def", "abc\ndef", "<select size='2'> contents of options preserved");
+testText("<select size='1'><option id='target'>abc</option><option>def", "abc", "<select size='1'> contents of target option preserved");
+testText("<select size='2'><option id='target'>abc</option><option>def", "abc", "<select size='2'> contents of target option preserved");
+testText("<div>a<select></select>bc", "abc", "empty <select>");
+testText("<div>a<select><optgroup></select>bc", "a\nbc", "empty <optgroup> in <select>");
+testText("<div>a<select><option></select>bc", "a\nbc", "empty <option> in <select>");
+testText("<select class='poke'></select>", "", "<select> containing text node child");
+testText("<select><optgroup class='poke-optgroup'></select>", "", "<optgroup> containing <optgroup>");
+testText("<select><optgroup><option>abc</select>", "abc", "<optgroup> containing <option>");
+testText("<select><option class='poke-div'>123</select>", "123\nabc", "<div> in <option>");
+testText("<div>a<optgroup></optgroup>bc", "a\nbc", "empty <optgroup> in <div>");
+testText("<div>a<optgroup>123</optgroup>bc", "a\nbc", "<optgroup> in <div>");
+testText("<div>a<option></option>bc", "a\nbc", "empty <option> in <div>");
+testText("<div>a<option>123</option>bc", "a\n123\nbc", "<option> in <div>");
+
+/**** innerText on replaced element children ****/
+
+testText("<div><button>abc", "abc", "<button> contents preserved");
+testText("<div><fieldset>abc", "abc", "<fieldset> contents preserved");
+testText("<div><fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
+testText("<div><input type='text' value='abc'>", "", "<input> contents ignored");
+testText("<div><textarea>abc", "", "<textarea> contents ignored");
+testText("<div><select size='1'><option>abc</option><option>def", "abc\ndef", "<select size='1'> contents of options preserved");
+testText("<div><select size='2'><option>abc</option><option>def", "abc\ndef", "<select size='2'> contents of options preserved");
+testText("<div><iframe>abc", "", "<iframe> contents ignored");
+testText("<div><iframe src='data:text/html,abc'>", ""," <iframe> subdocument ignored");
+testText("<div><audio>abc", "", "<audio> contents ignored");
+testText("<div><video>abc", "", "<video> contents ignored");
+testText("<div><canvas>abc", "", "<canvas> contents ignored");
+testText("<div><img alt='abc'>", "", "<img> alt text ignored");
+
+/**** Lines around blocks ****/
+
+testText("<div>123<div>abc</div>def", "123\nabc\ndef", "Newline at block boundary");
+testText("<div>123<span style='display:block'>abc</span>def", "123\nabc\ndef", "Newline at display:block boundary");
+testText("<div>abc<div></div>def", "abc\ndef", "Empty block induces single line break");
+testText("<div>abc<div></div><div></div>def", "abc\ndef", "Consecutive empty blocks ignored");
+testText("<div><p>abc", "abc", "No blank lines around <p> alone");
+testText("<div><p>abc</p> ", "abc", "No blank lines around <p> followed by only collapsible whitespace");
+testText("<div> <p>abc</p>", "abc", "No blank lines around <p> preceded by only collapsible whitespace");
+testText("<div><p>abc<p>def", "abc\n\ndef", "Blank line between consecutive <p>s");
+testText("<div><p>abc</p> <p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by collapsible whitespace");
+testText("<div><p>abc</p><div></div><p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by empty block");
+testText("<div><p>abc</p><div>123</div><p>def", "abc\n\n123\n\ndef", "Blank lines between <p>s separated by non-empty block");
+testText("<div>abc<div><p>123</p></div>def", "abc\n\n123\n\ndef", "Blank lines around a <p> in its own block");
+testText("<div>abc<p>def", "abc\n\ndef", "Blank line before <p>");
+testText("<div><p>abc</p>def", "abc\n\ndef", "Blank line after <p>");
+testText("<div><p>abc<p></p><p></p><p>def", "abc\n\ndef", "One blank line between <p>s, ignoring empty <p>s");
+testText("<div style='visibility:hidden'><p><span style='visibility:visible'>abc</span></p>\n<div style='visibility:visible'>def</div>",
+     "abc\ndef", "Invisible <p> doesn't induce extra line breaks");
+testText("<div>abc<div style='margin:2em'>def", "abc\ndef", "No blank lines around <div> with margin");
+testText("<div>123<span style='display:inline-block'>abc</span>def", "123abcdef", "No newlines at display:inline-block boundary");
+testText("<div>123<span style='display:inline-block'> abc </span>def", "123abcdef", "Leading/trailing space removal at display:inline-block boundary");
+testText("<div>123<p style='margin:0px'>abc</p>def", "123\n\nabc\n\ndef", "Blank lines around <p> even without margin");
+testText("<div>123<h1>abc</h1>def", "123\nabc\ndef", "No blank lines around <h1>");
+testText("<div>123<h2>abc</h2>def", "123\nabc\ndef", "No blank lines around <h2>");
+testText("<div>123<h3>abc</h3>def", "123\nabc\ndef", "No blank lines around <h3>");
+testText("<div>123<h4>abc</h4>def", "123\nabc\ndef", "No blank lines around <h4>");
+testText("<div>123<h5>abc</h5>def", "123\nabc\ndef", "No blank lines around <h5>");
+testText("<div>123<h6>abc</h6>def", "123\nabc\ndef", "No blank lines around <h6>");
+
+/**** Spans ****/
+
+testText("<div>123<span>abc</span>def", "123abcdef", "<span> boundaries are irrelevant");
+testText("<div>123 <span>abc</span> def", "123 abc def", "<span> boundaries are irrelevant");
+testText("<div style='width:0'>123 <span>abc</span> def", "123 abc def", "<span> boundaries are irrelevant");
+testText("<div>123<em>abc</em>def", "123abcdef", "<em> gets no special treatment");
+testText("<div>123<b>abc</b>def", "123abcdef", "<b> gets no special treatment");
+testText("<div>123<i>abc</i>def", "123abcdef", "<i> gets no special treatment");
+testText("<div>123<strong>abc</strong>def", "123abcdef", "<strong> gets no special treatment");
+testText("<div>123<tt>abc</tt>def", "123abcdef", "<tt> gets no special treatment");
+testText("<div>123<code>abc</code>def", "123abcdef", "<code> gets no special treatment");
+
+/**** Soft hyphen ****/
+
+testText("<div>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
+testText("<div style='width:0'>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
+
+/**** Tables ****/
+
+testText("<div><table style='white-space:pre'>  <td>abc</td>  </table>", "abc", "Ignoring non-rendered table whitespace");
+testText("<div><table><tr><td>abc<td>def</table>", "abc\tdef", "Tab-separated table cells");
+testText("<div><table><tr><td>abc<td><td>def</table>", "abc\t\tdef", "Tab-separated table cells including empty cells");
+testText("<div><table><tr><td>abc<td><td></table>", "abc\t\t", "Tab-separated table cells including trailing empty cells");
+testText("<div><table><tr><td>abc<tr><td>def</table>", "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<table><td>def</table>ghi", "abc\ndef\nghi", "Newlines around table");
+testText("<div><table style='border-collapse:collapse'><tr><td>abc<td>def</table>", "abc\tdef",
+         "Tab-separated table cells in a border-collapse table");
+testText("<div><table><tfoot>x</tfoot><tbody>y</tbody></table>", "xy", "tfoot not reordered");
+testText("<table><tfoot><tr><td>footer</tfoot><thead><tr><td style='visibility:collapse'>thead</thead><tbody><tr><td>tbody</tbody></table>",
+         "footer\n\ntbody", "");
+
+/**** Table captions ****/
+
+testText("<div><table><tr><td>abc<caption>def</caption></table>", "abc\ndef", "Newline between cells and caption");
+
+/**** display:table ****/
+
+testText("<div><div class='table'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>",
+         "abc\tdef", "Tab-separated table cells");
+testText("<div><div class='table'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
+         "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<div class='table'><span class='cell'>def</span></div>ghi", "abc\ndef\nghi", "Newlines around table");
+
+/**** display:inline-table ****/
+
+testText("<div><div class='itable'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>", "abc\tdef", "Tab-separated table cells");
+testText("<div><div class='itable'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
+         "abc\ndef", "Newline-separated table rows");
+testText("<div>abc<div class='itable'><span class='cell'>def</span></div>ghi", "abcdefghi", "No newlines around inline-table");
+testText("<div>abc<div class='itable'><span class='row'><span class='cell'>def</span></span>\n<span class='row'><span class='cell'>123</span></span></div>ghi",
+         "abcdef\n123ghi", "Single newline in two-row inline-table");
+
+/**** Lists ****/
+
+testText("<div><ol><li>abc", "abc", "<ol> list items get no special treatment");
+testText("<div><ul><li>abc", "abc", "<ul> list items get no special treatment");
+
+/**** Misc elements ****/
+
+testText("<div><script style='display:block'>abc", "abc", "display:block <script> is rendered");
+testText("<div><style style='display:block'>abc", "abc", "display:block <style> is rendered");
+testText("<div><noscript style='display:block'>abc", "", "display:block <noscript> is not rendered (it's not parsed!)");
+testText("<div><template style='display:block'>abc", "",
+         "display:block <template> contents are not rendered (the contents are in a different document)");
+testText("<div>abc<br>def", "abc\ndef", "<br> induces line break");
+testText("<div>abc<br>", "abc\n", "<br> induces line break even at end of block");
+testText("<div><br class='poke'>", "\n", "<br> content ignored");
+testText("<div>abc<hr>def", "abc\ndef", "<hr> induces line break");
+testText("<div>abc<hr><hr>def", "abc\ndef", "<hr><hr> induces just one line break");
+testText("<div>abc<hr><hr><hr>def", "abc\ndef", "<hr><hr><hr> induces just one line break");
+testText("<div><hr class='poke'>", "abc", "<hr> content rendered");
+testText("<div>abc<!--comment-->def", "abcdef", "comment ignored");
+
+/**** text-transform ****/
+
+testText("<div><div style='text-transform:uppercase'>abc", "ABC", "text-transform is applied");
+testText("<div><div style='text-transform:uppercase'>Ma\xDF", "MASS", "text-transform handles es-zet");
+testText("<div><div lang='tr' style='text-transform:uppercase'>i \u0131", "\u0130 I", "text-transform handles Turkish casing");
+
+/**** block-in-inline ****/
+
+testText("<div>abc<span>123<div>456</div>789</span>def", "abc123\n456\n789def", "block-in-inline doesn't add unnecessary newlines");
+
+/**** floats ****/
+
+testText("<div>abc<div style='float:left'>123</div>def", "abc\n123\ndef", "floats induce a block boundary");
+testText("<div>abc<span style='float:left'>123</span>def", "abc\n123\ndef", "floats induce a block boundary");
+
+/**** position ****/
+
+testText("<div>abc<div style='position:absolute'>123</div>def", "abc\n123\ndef", "position:absolute induces a block boundary");
+testText("<div>abc<span style='position:absolute'>123</span>def", "abc\n123\ndef", "position:absolute induces a block boundary");
+testText("<div>abc<div style='position:relative'>123</div>def", "abc\n123\ndef", "position:relative has no effect");
+testText("<div>abc<span style='position:relative'>123</span>def", "abc123def", "position:relative has no effect");
+
+/**** text-overflow:ellipsis ****/
+
+testText("<div style='overflow:hidden'>abc", "abc", "overflow:hidden ignored");
+// XXX Chrome skips content with width:0 or height:0 and overflow:hidden;
+// should we spec that?
+testText("<div style='width:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero width");
+testText("<div style='height:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero height");
+testText("<div style='width:0; overflow:hidden; text-overflow:ellipsis'>abc", "abc", "text-overflow:ellipsis ignored");
+
+/**** Support on non-HTML elements ****/
+
+testText("<svg>abc", undefined, "innerText not supported on SVG elements");
+testText("<math>abc", undefined, "innerText not supported on MathML elements");
+
+/**** Ruby ****/
+
+testText("<div><ruby>abc<rt>def</rt></ruby>", "abcdef", "<rt> and no <rp>");
+testText("<div><ruby>abc<rp>(</rp><rt>def</rt><rp>)</rp></ruby>", "abcdef", "<rp>");
+testText("<div><rp>abc</rp>", "", "Lone <rp>");
+testText("<div><rp style='visibility:hidden'>abc</rp>", "", "visibility:hidden <rp>");
+testText("<div><rp style='display:block'>abc</rp>def", "abc\ndef", "display:block <rp>");
+testText("<div><rp style='display:block'> abc </rp>def", "abc\ndef", "display:block <rp> with whitespace");
+testText("<div><select class='poke-rp'></select>", "", "<rp> in a <select>");
+
+/**** Shadow DOM ****/
+
+if ("attachShadow" in document.body) {
+  testText("<div class='shadow'>", "", "Shadow DOM contents ignored");
+  testText("<div><div class='shadow'>", "", "Shadow DOM contents ignored");
+}
+
+/**** Flexbox ****/
+
+if (CSS.supports('display', 'flex')) {
+  testText("<div style='display:flex'><div style='order:1'>1</div><div>2</div></div>",
+           "1\n2", "CSS 'order' property ignored");
+  testText("<div style='display:flex'><span>1</span><span>2</span></div>",
+           "1\n2", "Flex items blockified");
+}
+
+/**** Grid ****/
+
+if (CSS.supports('display', 'grid')) {
+  testText("<div style='display:grid'><div style='order:1'>1</div><div>2</div></div>",
+           "1\n2", "CSS 'order' property ignored");
+  testText("<div style='display:grid'><span>1</span><span>2</span></div>",
+           "1\n2", "Grid items blockified");
+}
diff --git a/html/dom/elements/the-innertext-idl-attribute/getter.html b/html/dom/elements/the-innertext-idl-attribute/getter.html
new file mode 100644
index 0000000..c84bb04
--- /dev/null
+++ b/html/dom/elements/the-innertext-idl-attribute/getter.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<title>innerText getter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.before::before { content:'abc'; }
+.table { display:table; }
+.itable { display:inline-table; }
+.row { display:table-row; }
+.cell { display:table-cell; }
+.first-line-uppercase::first-line { text-transform:uppercase; }
+.first-letter-uppercase::first-letter { text-transform:uppercase; }
+.first-letter-float::first-letter { float:left; }
+</style>
+<div id="container"></div>
+<svg id="svgContainer"></svg>
+<script>
+let container = document.querySelector('#container');
+let svgContainer = document.querySelector('#svgContainer');
+function testText(html, expectedPlain, msg) {
+  textTextInContainer(container, html, expectedPlain, msg);
+}
+function testTextInSVG(html, expectedPlain, msg) {
+  textTextInContainer(svgContainer, html, expectedPlain, msg);
+}
+function textTextInContainer(cont, html, expectedPlain, msg) {
+  test(function() {
+    container.innerHTML = html;
+    if (cont != container) {
+      while (container.firstChild) {
+        cont.appendChild(container.firstChild);
+      }
+    }
+    var e = document.getElementById('target');
+    if (!e) {
+      e = cont.firstChild;
+    }
+    var pokes = document.getElementsByClassName('poke');
+    for (var i = 0; i < pokes.length; ++i) {
+      pokes[i].textContent = 'abc';
+    }
+    ['rp', 'optgroup', 'div'].forEach(function(tag) {
+      pokes = document.getElementsByClassName('poke-' + tag);
+      for (var i = 0; i < pokes.length; ++i) {
+        var el = document.createElement(tag);
+        el.textContent = "abc";
+        pokes[i].appendChild(el);
+      }
+    });
+    var shadows = document.getElementsByClassName('shadow');
+    for (var i = 0; i < shadows.length; ++i) {
+      var s = shadows[i].attachShadow({ mode: "open" });
+      s.textContent = 'abc';
+    }
+    while (e && e.nodeType != Node.ELEMENT_NODE) {
+      e = e.nextSibling;
+    }
+    assert_equals(e.innerText, expectedPlain);
+    cont.textContent = '';
+  }, msg + ' (' + format_value(html) + ')');
+}
+</script>
+<script src="getter-tests.js"></script>
diff --git a/innerText/multiple-text-nodes.window.js b/html/dom/elements/the-innertext-idl-attribute/multiple-text-nodes.window.js
similarity index 100%
rename from innerText/multiple-text-nodes.window.js
rename to html/dom/elements/the-innertext-idl-attribute/multiple-text-nodes.window.js
diff --git a/innerText/setter-tests.js b/html/dom/elements/the-innertext-idl-attribute/setter-tests.js
similarity index 100%
rename from innerText/setter-tests.js
rename to html/dom/elements/the-innertext-idl-attribute/setter-tests.js
diff --git a/innerText/setter.html b/html/dom/elements/the-innertext-idl-attribute/setter.html
similarity index 100%
rename from innerText/setter.html
rename to html/dom/elements/the-innertext-idl-attribute/setter.html
diff --git a/html/semantics/document-metadata/the-link-element/link-rel-attribute.html b/html/semantics/document-metadata/the-link-element/link-rel-attribute.html
index 4492f88..14d0622 100644
--- a/html/semantics/document-metadata/the-link-element/link-rel-attribute.html
+++ b/html/semantics/document-metadata/the-link-element/link-rel-attribute.html
@@ -16,11 +16,6 @@
 </div>
 
 <script>
-var host = document.querySelector("#host");
-var shadow = host.attachShadow({ mode: "open" });
-var tmpl = document.querySelector("template#shadow-dom");
-var clone = document.importNode(tmpl.content, true);
-shadow.appendChild(clone);
 
 function testLinkRelModification(testDiv, testLink) {
   assert_equals(getComputedStyle(testDiv).color, "rgb(0, 128, 0)");
@@ -38,6 +33,11 @@
 }, "Removing stylesheet from link rel attribute should remove the stylesheet for light DOM");
 
 test (() => {
+  var host = document.querySelector("#host");
+  var shadow = host.attachShadow({ mode: "open" });
+  var tmpl = document.querySelector("template#shadow-dom");
+  var clone = document.importNode(tmpl.content, true);
+  shadow.appendChild(clone);
   testLinkRelModification(shadow.querySelector("#shadow-div"),
                           shadow.querySelector("#shadow-link"));
 }, "Removing stylesheet from link rel attribute should remove the stylesheet for shadow DOM");
diff --git a/html/semantics/forms/form-submission-0/constructing-form-data-set.html b/html/semantics/forms/form-submission-0/constructing-form-data-set.html
new file mode 100644
index 0000000..0fe9171
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/constructing-form-data-set.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set">
+<link ref="help" href="https://xhr.spec.whatwg.org/#dom-formdata">
+<link rel="help" href="https://fetch.spec.whatwg.org/#concept-bodyinit-extract">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+
+<iframe name="frame1"></iframe>
+<form accept-charset="iso-8859-1" target="frame1" action="/common/blank.html">
+<input type="hidden" name="_charset_">
+</form>
+
+<script>
+test(() => {
+  let formData = new FormData(document.querySelector('form'));
+  assert_equals(formData.get('_charset_'), 'UTF-8');
+}, 'FormData constructor always produces UTF-8 _charset_ value.');
+
+let t = async_test('_charset_ control sets the expected encoding name.');
+t.step(() => {
+  let iframe = document.querySelector('iframe');
+  iframe.onload = t.step_func_done(() => {
+    assert_not_equals(iframe.contentDocument.URL.indexOf('_charset_=windows-1252'), -1);
+  });
+  document.querySelector('form').submit();
+});
+</script>
+</body>
diff --git a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html
deleted file mode 100644
index 666bc7c..0000000
--- a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a classic script</title>
-<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
-<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
-
-<script src="/resources/testharness.js" integrity="sha384-4Nybydhnr3tOpv1yrTkDxu3RFpnxWAxlU5kGn7c8ebKvh1iUdfVMjqP6jf0dacrV"></script>
-<script src="/resources/testharnessreport.js" integrity="sha384-GOnHxuyo+nnsFAe4enY+RAl4/+w5NPMJPCQiDroTjxtR7ndRz7Uan8vNbM2qWKmU"></script>
-
-<div id="dummy"></div>
-
-<script>
-function createTestPromise() {
-  return new Promise((resolve, reject) => {
-    window.continueTest = resolve;
-    window.errorTest = reject;
-  });
-}
-
-const dummyDiv = document.querySelector("#dummy");
-
-const evaluators = {
-  eval,
-  setTimeout,
-  "the Function constructor"(x) {
-    Function(x)();
-  },
-  "reflected inline event handlers"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.onclick();
-  },
-  "inline event handlers triggered via UA code"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.click(); // different from .**on**click()
-  }
-};
-
-for (const [label, evaluator] of Object.entries(evaluators)) {
-  promise_test(t => {
-    t.add_cleanup(() => {
-      dummyDiv.removeAttribute("onclick");
-      delete window.evaluated_imports_a;
-    });
-
-    const promise = createTestPromise();
-
-    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
-
-    return promise_rejects(t, new TypeError(), promise);
-  }, label + " should fail to import");
-};
-</script>
diff --git a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html
new file mode 100644
index 0000000..80a2989
--- /dev/null
+++ b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a classic script</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
+
+<script src="/resources/testharness.js" integrity="sha384-{{file_hash(sha384, resources/testharness.js)}}"></script>
+<script src="/resources/testharnessreport.js" integrity="sha384-{{file_hash(sha384, resources/testharnessreport.js)}}"></script>
+
+<div id="dummy"></div>
+
+<script>
+function createTestPromise() {
+  return new Promise((resolve, reject) => {
+    window.continueTest = resolve;
+    window.errorTest = reject;
+  });
+}
+
+const dummyDiv = document.querySelector("#dummy");
+
+const evaluators = {
+  eval,
+  setTimeout,
+  "the Function constructor"(x) {
+    Function(x)();
+  },
+  "reflected inline event handlers"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.onclick();
+  },
+  "inline event handlers triggered via UA code"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.click(); // different from .**on**click()
+  }
+};
+
+for (const [label, evaluator] of Object.entries(evaluators)) {
+  promise_test(t => {
+    t.add_cleanup(() => {
+      dummyDiv.removeAttribute("onclick");
+      delete window.evaluated_imports_a;
+    });
+
+    const promise = createTestPromise();
+
+    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
+
+    return promise_rejects(t, new TypeError(), promise);
+  }, label + " should fail to import");
+};
+</script>
diff --git a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html
deleted file mode 100644
index 497c9d9..0000000
--- a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a module script</title>
-<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
-<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
-
-<script src="/resources/testharness.js" integrity="sha384-4Nybydhnr3tOpv1yrTkDxu3RFpnxWAxlU5kGn7c8ebKvh1iUdfVMjqP6jf0dacrV"></script>
-<script src="/resources/testharnessreport.js" integrity="sha384-GOnHxuyo+nnsFAe4enY+RAl4/+w5NPMJPCQiDroTjxtR7ndRz7Uan8vNbM2qWKmU"></script>
-
-<div id="dummy"></div>
-
-<script type="module">
-function createTestPromise() {
-  return new Promise((resolve, reject) => {
-    window.continueTest = resolve;
-    window.errorTest = reject;
-  });
-}
-
-const dummyDiv = document.querySelector("#dummy");
-
-const evaluators = {
-  eval,
-  setTimeout,
-  "the Function constructor"(x) {
-    Function(x)();
-  },
-  "reflected inline event handlers"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.onclick();
-  },
-  "inline event handlers triggered via UA code"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.click(); // different from .**on**click()
-  }
-};
-
-for (const [label, evaluator] of Object.entries(evaluators)) {
-  promise_test(t => {
-    t.add_cleanup(() => {
-      dummyDiv.removeAttribute("onclick");
-      delete window.evaluated_imports_a;
-    });
-
-    const promise = createTestPromise();
-
-    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
-
-    return promise_rejects(t, new TypeError(), promise);
-  }, label + " should fail to import");
-};
-</script>
diff --git a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html
new file mode 100644
index 0000000..db3d3b1
--- /dev/null
+++ b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a module script</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
+
+<script src="/resources/testharness.js" integrity="sha384-{{file_hash(sha384, resources/testharness.js)}}"></script>
+<script src="/resources/testharnessreport.js" integrity="sha384-{{file_hash(sha384, resources/testharnessreport.js)}}"></script>
+
+<div id="dummy"></div>
+
+<script type="module">
+function createTestPromise() {
+  return new Promise((resolve, reject) => {
+    window.continueTest = resolve;
+    window.errorTest = reject;
+  });
+}
+
+const dummyDiv = document.querySelector("#dummy");
+
+const evaluators = {
+  eval,
+  setTimeout,
+  "the Function constructor"(x) {
+    Function(x)();
+  },
+  "reflected inline event handlers"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.onclick();
+  },
+  "inline event handlers triggered via UA code"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.click(); // different from .**on**click()
+  }
+};
+
+for (const [label, evaluator] of Object.entries(evaluators)) {
+  promise_test(t => {
+    t.add_cleanup(() => {
+      dummyDiv.removeAttribute("onclick");
+      delete window.evaluated_imports_a;
+    });
+
+    const promise = createTestPromise();
+
+    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
+
+    return promise_rejects(t, new TypeError(), promise);
+  }, label + " should fail to import");
+};
+</script>
diff --git a/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-of-promise-result.html b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-of-promise-result.html
new file mode 100644
index 0000000..e0e3ec8
--- /dev/null
+++ b/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-of-promise-result.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>import() inside compiled strings inside a classic script</title>
+<link rel="help" href="https://github.com/whatwg/html/pull/3163">
+<link rel="help" href="https://github.com/tc39/ecma262/issues/871#issuecomment-292493142">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+self.ran = false;
+
+promise_test(t => {
+  t.add_cleanup(() => {
+    self.ran = false;
+  })
+
+  return Promise.resolve(`import("../imports-a.js?1").then(() => { self.ran = true; })`)
+    .then(eval)
+    .then(() => {
+      assert_true(self.ran);
+    });
+}, "Evaled the script via eval, successful import");
+
+promise_test(t => {
+  t.add_cleanup(() => {
+    self.ran = false;
+  })
+
+  return Promise.resolve(`import("bad-specifier?1").catch(() => { self.ran = true; })`)
+    .then(eval)
+    .then(() => {
+      assert_true(self.ran);
+    });
+}, "Evaled the script via eval, failed import");
+
+promise_test(t => {
+  t.add_cleanup(() => {
+    self.ran = false;
+  })
+
+  return Promise.resolve(`return import("../imports-a.js?2").then(() => { self.ran = true; })`)
+    .then(Function)
+    .then(Function.prototype.call.bind(Function.prototype.call))
+    .then(() => {
+      assert_true(self.ran);
+    });
+}, "Evaled the script via Function, successful import");
+
+promise_test(t => {
+  t.add_cleanup(() => {
+    self.ran = false;
+  })
+
+  return Promise.resolve(`return import("bad-specifier?2").catch(() => { self.ran = true; })`)
+    .then(Function)
+    .then(Function.prototype.call.bind(Function.prototype.call))
+    .then(() => {
+      assert_true(self.ran);
+    });
+}, "Evaled the script via Function, failed import");
+</script>
diff --git a/html/user-activation/activation-thru-contextmenu-event-manual.html b/html/user-activation/activation-thru-contextmenu-event-manual.html
new file mode 100644
index 0000000..96d17e2
--- /dev/null
+++ b/html/user-activation/activation-thru-contextmenu-event-manual.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>User activation with 'contextmenu' event</title>
+    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
+    <link rel="author" title="Google" href="http://www.google.com "/>
+    <link rel="help" href="https://html.spec.whatwg.org/#triggered-by-user-activation">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <style>
+      #target {
+        width: 250px;
+        height: 150px;
+        float: left;
+        background-color: green;
+      }
+
+      #done {
+        float: left;
+        padding: 20px;
+        margin: 10px;
+      }
+    </style>
+    <script type="text/javascript">
+      let activation_event_fired = false;
+
+      function run() {
+        let success = false;
+        let test_contextmenu = async_test("'contextmenu' can call vibrate.");
+
+        on_event(document.getElementById("done"), "click", () => {
+          test_contextmenu.step(() => {
+            assert_true(activation_event_fired, "activation event has fired");
+          });
+          test_contextmenu.done();
+        });
+
+        on_event(document.getElementById("target"), "contextmenu", (e) => {
+            test_contextmenu.step(() => {
+              e.preventDefault();
+              assert_true(navigator.vibrate(200), "navigator.vibrate is successful");
+              activation_event_fired = true;
+            });
+        });
+      }
+    </script>
+  </head>
+  <body onload="run()">
+    <h1>User activation with 'contextmenu' event</h1>
+    <h4>Tests that a 'contextmenu' event is treated like a user activation.</h4>
+    <ol>
+      <li>Right-click or long-press on green.</li>
+      <li>Click or tap on Done.</li>
+    </ol>
+    <div id="target"></div>
+    <input type="button" id="done" value="Done" />
+  </body>
+</html>
diff --git a/images/wpt-logo/wpt-logo-darkblue-bg.svg b/images/wpt-logo/wpt-logo-darkblue-bg.svg
new file mode 100644
index 0000000..49f374c
--- /dev/null
+++ b/images/wpt-logo/wpt-logo-darkblue-bg.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 762 762">
+<rect x="0" y="0" fill="#003C56" width="762" height="762"/>
+<rect x="183.9" y="511.4" fill="#FFFFFF" width="201.5" height="38"/>
+<path fill="#FFFFFF" d="M504.9,549.4c37.3,0,67.6-30.3,67.6-67.6v-19h-38v19c0,16.3-13.3,29.6-29.6,29.6c-16.3,0-29.6-13.3-29.6-29.6
+V287.4h-38v56.5h-51.9v-92.6h112.4c25.5,0,46.3,20.8,46.3,46.3c0,20-13,37.8-31.8,44v39.1c40-6.9,69.8-42.1,69.8-83
+c0-46.5-37.8-84.3-84.3-84.3H366.4c-10.5,0-19,8.5-19,19V386l-46.4-79.4c-3.4-5.8-9.7-9.4-16.4-9.4c-6.7,0-13,3.6-16.4,9.4
+l-46.2,79.2V213.4h-38v242.7c0,8.6,5.8,16.1,14,18.3c8.3,2.2,17.1-1.4,21.4-8.8l65.2-111.8L350,465.7c3.4,5.8,9.7,9.4,16.4,9.4
+c1.7,0,3.3-0.2,5-0.7c8.3-2.2,14-9.8,14-18.3v-74.2h51.9v99.8C437.3,519.1,467.6,549.4,504.9,549.4z"/>
+</svg>
diff --git a/images/wpt-logo/wpt-logo-darkblue.svg b/images/wpt-logo/wpt-logo-darkblue.svg
new file mode 100644
index 0000000..2db07da
--- /dev/null
+++ b/images/wpt-logo/wpt-logo-darkblue.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 398 340">
+<rect x="0" y="300" fill="#003C56" width="201.5" height="38"/>
+<path fill="#003C56" d="M320.9,338c37.3,0,67.6-30.3,67.6-67.6v-19h-38v19c0,16.3-13.3,29.6-29.6,29.6c-16.3,0-29.6-13.3-29.6-29.6V76
+h-38v56.5h-51.9V40h112.4c25.5,0,46.3,20.8,46.3,46.3c0,20-13,37.8-31.8,44v39.1c40-6.9,69.8-42.1,69.8-83
+C398,39.8,360.2,2,313.8,2H182.4c-10.5,0-19,8.5-19,19v153.6l-46.4-79.4c-3.4-5.8-9.7-9.4-16.4-9.4c-6.7,0-13,3.6-16.4,9.4
+l-46.2,79.2V2h-38v242.7c0,8.6,5.8,16.1,14,18.3c8.3,2.2,17.1-1.4,21.4-8.8l65.2-111.8L166,254.3c3.4,5.8,9.7,9.4,16.4,9.4
+c1.7,0,3.3-0.2,5-0.7c8.3-2.2,14-9.8,14-18.3v-74.2h51.9v99.8C253.3,307.7,283.6,338,320.9,338z"/>
+</svg>
diff --git a/images/wpt-logo/wpt-logo-lightblue-bg.svg b/images/wpt-logo/wpt-logo-lightblue-bg.svg
new file mode 100644
index 0000000..2f61672
--- /dev/null
+++ b/images/wpt-logo/wpt-logo-lightblue-bg.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 762 762">
+<rect x="0" y="0" fill="#78D9F4" width="762" height="762"/>
+<rect x="183.9" y="511.4" fill="#003C56" width="201.5" height="38"/>
+<path fill="#003C56" d="M504.9,549.4c37.3,0,67.6-30.3,67.6-67.6v-19h-38v19c0,16.3-13.3,29.6-29.6,29.6c-16.3,0-29.6-13.3-29.6-29.6
+V287.4h-38v56.5h-51.9v-92.6h112.4c25.5,0,46.3,20.8,46.3,46.3c0,20-13,37.8-31.8,44v39.1c40-6.9,69.8-42.1,69.8-83
+c0-46.5-37.8-84.3-84.3-84.3H366.4c-10.5,0-19,8.5-19,19V386l-46.4-79.4c-3.4-5.8-9.7-9.4-16.4-9.4c-6.7,0-13,3.6-16.4,9.4
+l-46.2,79.2V213.4h-38v242.7c0,8.6,5.8,16.1,14,18.3c8.3,2.2,17.1-1.4,21.4-8.8l65.2-111.8L350,465.7c3.4,5.8,9.7,9.4,16.4,9.4
+c1.7,0,3.3-0.2,5-0.7c8.3-2.2,14-9.8,14-18.3v-74.2h51.9v99.8C437.3,519.1,467.6,549.4,504.9,549.4z"/>
+</svg>
diff --git a/images/wpt-logo/wpt-logo-orange-bg.svg b/images/wpt-logo/wpt-logo-orange-bg.svg
new file mode 100644
index 0000000..fde2c15
--- /dev/null
+++ b/images/wpt-logo/wpt-logo-orange-bg.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 762 762">
+<rect x="0" y="0" fill="#E86E43" width="762" height="762"/>
+<rect x="183.9" y="511.4" fill="#FFFFFF" width="201.5" height="38"/>
+<path fill="#FFFFFF" d="M504.9,549.4c37.3,0,67.6-30.3,67.6-67.6v-19h-38v19c0,16.3-13.3,29.6-29.6,29.6c-16.3,0-29.6-13.3-29.6-29.6
+V287.4h-38v56.5h-51.9v-92.6h112.4c25.5,0,46.3,20.8,46.3,46.3c0,20-13,37.8-31.8,44v39.1c40-6.9,69.8-42.1,69.8-83
+c0-46.5-37.8-84.3-84.3-84.3H366.4c-10.5,0-19,8.5-19,19V386l-46.4-79.4c-3.4-5.8-9.7-9.4-16.4-9.4c-6.7,0-13,3.6-16.4,9.4
+l-46.2,79.2V213.4h-38v242.7c0,8.6,5.8,16.1,14,18.3c8.3,2.2,17.1-1.4,21.4-8.8l65.2-111.8L350,465.7c3.4,5.8,9.7,9.4,16.4,9.4
+c1.7,0,3.3-0.2,5-0.7c8.3-2.2,14-9.8,14-18.3v-74.2h51.9v99.8C437.3,519.1,467.6,549.4,504.9,549.4z"/>
+</svg>
diff --git a/images/wpt-logo/wpt-logo-white.svg b/images/wpt-logo/wpt-logo-white.svg
new file mode 100644
index 0000000..4cab7b5
--- /dev/null
+++ b/images/wpt-logo/wpt-logo-white.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 398 340">
+<rect x="0" y="300" fill="#FFFFFF" width="201.5" height="38"/>
+<path fill="#FFFFFF" d="M320.9,338c37.3,0,67.6-30.3,67.6-67.6v-19h-38v19c0,16.3-13.3,29.6-29.6,29.6c-16.3,0-29.6-13.3-29.6-29.6V76
+h-38v56.5h-51.9V40h112.4c25.5,0,46.3,20.8,46.3,46.3c0,20-13,37.8-31.8,44v39.1c40-6.9,69.8-42.1,69.8-83
+C398,39.8,360.2,2,313.8,2H182.4c-10.5,0-19,8.5-19,19v153.6l-46.4-79.4c-3.4-5.8-9.7-9.4-16.4-9.4c-6.7,0-13,3.6-16.4,9.4
+l-46.2,79.2V2h-38v242.7c0,8.6,5.8,16.1,14,18.3c8.3,2.2,17.1-1.4,21.4-8.8l65.2-111.8L166,254.3c3.4,5.8,9.7,9.4,16.4,9.4
+c1.7,0,3.3-0.2,5-0.7c8.3-2.2,14-9.8,14-18.3v-74.2h51.9v99.8C253.3,307.7,283.6,338,320.9,338z"/>
+</svg>
diff --git a/infrastructure/OWNERS b/infrastructure/OWNERS
new file mode 100644
index 0000000..684dcf3
--- /dev/null
+++ b/infrastructure/OWNERS
@@ -0,0 +1,4 @@
+@gsnedders
+@jgraham
+@jugglinmike
+@kereliuk
diff --git a/infrastructure/README.md b/infrastructure/README.md
index 28a764d..82138a3 100644
--- a/infrastructure/README.md
+++ b/infrastructure/README.md
@@ -4,4 +4,6 @@
  * The tests in assumptions/ are designed to test UA assumptions
    documented in [assumptions.md](/docs/_writing-tests/assumptions.md).
 
+ * The tests in server/ are designed to test the WPT server configuration
+
  * The tests in expected-fail/ should all fail.
diff --git a/infrastructure/browsers/firefox/prefs.html b/infrastructure/browsers/firefox/prefs.html
new file mode 100644
index 0000000..64a985a
--- /dev/null
+++ b/infrastructure/browsers/firefox/prefs.html
@@ -0,0 +1,7 @@
+<title>Ensure that setting gecko prefs works</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+assert_equals(getComputedStyle(document.documentElement).color, "rgb(0, 255, 0)")
+</script>
+<p>This should be green</p>
diff --git a/infrastructure/metadata/infrastructure/browsers/firefox/__dir__.ini b/infrastructure/metadata/infrastructure/browsers/firefox/__dir__.ini
new file mode 100644
index 0000000..3e0ed18
--- /dev/null
+++ b/infrastructure/metadata/infrastructure/browsers/firefox/__dir__.ini
@@ -0,0 +1,2 @@
+disabled:
+  if product != "firefox": true
\ No newline at end of file
diff --git a/infrastructure/metadata/infrastructure/browsers/firefox/prefs.html.ini b/infrastructure/metadata/infrastructure/browsers/firefox/prefs.html.ini
new file mode 100644
index 0000000..7b78d21
--- /dev/null
+++ b/infrastructure/metadata/infrastructure/browsers/firefox/prefs.html.ini
@@ -0,0 +1,2 @@
+[prefs.html]
+  prefs: ["browser.display.foreground_color:#00FF00"]
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_and_fail.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_and_fail.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_and_fail.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_and_fail.html.ini
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_cycle_fail.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_cycle_fail.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_cycle_fail.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_cycle_fail.html.ini
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_match_fail.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_match_fail.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_match_fail.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_match_fail.html.ini
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_mismatch_fail.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_mismatch_fail.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_mismatch_fail.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_mismatch_fail.html.ini
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_ref_timeout.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_ref_timeout.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_ref_timeout.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_ref_timeout.html.ini
diff --git a/tools/wptrunner/test/metadata/reftest/reftest_timeout.html.ini b/infrastructure/metadata/infrastructure/reftest/reftest_timeout.html.ini
similarity index 100%
rename from tools/wptrunner/test/metadata/reftest/reftest_timeout.html.ini
rename to infrastructure/metadata/infrastructure/reftest/reftest_timeout.html.ini
diff --git a/tools/wptrunner/test/testdata/reftest/green-ref.html b/infrastructure/reftest/green-ref.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/green-ref.html
rename to infrastructure/reftest/green-ref.html
diff --git a/tools/wptrunner/test/testdata/reftest/green.html b/infrastructure/reftest/green.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/green.html
rename to infrastructure/reftest/green.html
diff --git a/tools/wptrunner/test/testdata/reftest/red.html b/infrastructure/reftest/red.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/red.html
rename to infrastructure/reftest/red.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest.https.html b/infrastructure/reftest/reftest.https.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest.https.html
rename to infrastructure/reftest/reftest.https.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_and_fail.html b/infrastructure/reftest/reftest_and_fail.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_and_fail.html
rename to infrastructure/reftest/reftest_and_fail.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_and_fail_0-ref.html b/infrastructure/reftest/reftest_and_fail_0-ref.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_and_fail_0-ref.html
rename to infrastructure/reftest/reftest_and_fail_0-ref.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_cycle.html b/infrastructure/reftest/reftest_cycle.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_cycle.html
rename to infrastructure/reftest/reftest_cycle.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_cycle_0-ref.html b/infrastructure/reftest/reftest_cycle_0-ref.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_cycle_0-ref.html
rename to infrastructure/reftest/reftest_cycle_0-ref.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_cycle_1-ref.html b/infrastructure/reftest/reftest_cycle_1-ref.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_cycle_1-ref.html
rename to infrastructure/reftest/reftest_cycle_1-ref.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_cycle_fail.html b/infrastructure/reftest/reftest_cycle_fail.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_cycle_fail.html
rename to infrastructure/reftest/reftest_cycle_fail.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_cycle_fail_0-ref.html b/infrastructure/reftest/reftest_cycle_fail_0-ref.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_cycle_fail_0-ref.html
rename to infrastructure/reftest/reftest_cycle_fail_0-ref.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_match.html b/infrastructure/reftest/reftest_match.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_match.html
rename to infrastructure/reftest/reftest_match.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_match_fail.html b/infrastructure/reftest/reftest_match_fail.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_match_fail.html
rename to infrastructure/reftest/reftest_match_fail.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_mismatch.html b/infrastructure/reftest/reftest_mismatch.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_mismatch.html
rename to infrastructure/reftest/reftest_mismatch.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_mismatch_fail.html b/infrastructure/reftest/reftest_mismatch_fail.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_mismatch_fail.html
rename to infrastructure/reftest/reftest_mismatch_fail.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_or_0.html b/infrastructure/reftest/reftest_or_0.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_or_0.html
rename to infrastructure/reftest/reftest_or_0.html
diff --git a/infrastructure/reftest/reftest_ref_timeout-ref.html b/infrastructure/reftest/reftest_ref_timeout-ref.html
new file mode 100644
index 0000000..2f52c05
--- /dev/null
+++ b/infrastructure/reftest/reftest_ref_timeout-ref.html
@@ -0,0 +1,5 @@
+<html class="reftest-wait">
+<title>rel=match that should time out in the ref</title>
+<style>
+:root {background-color:green}
+</style>
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_ref_timeout.html b/infrastructure/reftest/reftest_ref_timeout.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_ref_timeout.html
rename to infrastructure/reftest/reftest_ref_timeout.html
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_timeout.html b/infrastructure/reftest/reftest_timeout.html
similarity index 100%
rename from tools/wptrunner/test/testdata/reftest/reftest_timeout.html
rename to infrastructure/reftest/reftest_timeout.html
diff --git a/infrastructure/reftest/reftest_wait_0.html b/infrastructure/reftest/reftest_wait_0.html
new file mode 100644
index 0000000..fec62a3
--- /dev/null
+++ b/infrastructure/reftest/reftest_wait_0.html
@@ -0,0 +1,13 @@
+<html class="reftest-wait">
+<title>Test with reftest-wait</title>
+<link rel=match href=green.html>
+<style>
+:root {background-color:red}
+</style>
+<script>
+setTimeout(function() {
+  document.documentElement.style.backgroundColor = "green";
+  document.documentElement.className = "";
+}, 2000);
+</script>
+</html>
diff --git a/infrastructure/assumptions/secure-context.https.any.js b/infrastructure/server/secure-context.https.any.js
similarity index 100%
rename from infrastructure/assumptions/secure-context.https.any.js
rename to infrastructure/server/secure-context.https.any.js
diff --git a/infrastructure/server/wpt-server-http.sub.html b/infrastructure/server/wpt-server-http.sub.html
new file mode 100644
index 0000000..438a6e6
--- /dev/null
+++ b/infrastructure/server/wpt-server-http.sub.html
@@ -0,0 +1,172 @@
+<!doctype html>
+<html>
+  <head>
+    <title>WPT Server checker</title>
+    <meta charset="utf-8" />
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+  </head>
+</body>
+<body>
+<script>
+function check(protocol, domain, port, done) {
+  var url = protocol + '://' + domain + ':' + port + '/media/1x1-green.png';
+  var img = document.createElement('img');
+  img.setAttribute('src', url);
+  img.style.display = 'none';
+  img.onerror = function() {
+    done(false);
+  };
+  img.onload = function() {
+    done(true);
+  };
+
+  document.body.appendChild(img);
+}
+
+async_test(function(t) {
+  check('http', '{{browser_host}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, no subdomain, port #1');
+
+async_test(function(t) {
+  check('http', '{{browser_host}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, no subdomain, port #2');
+
+async_test(function(t) {
+  check('http', '{{domains[www]}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #1, port #1');
+
+async_test(function(t) {
+  check('http', '{{domains[www]}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #1, port #2');
+
+async_test(function(t) {
+  check('http', '{{domains[www1]}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #2, port #1');
+
+async_test(function(t) {
+  check('http', '{{domains[www1]}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #2, port #2');
+
+async_test(function(t) {
+  check('http', '{{domains[www2]}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #3, port #1');
+
+async_test(function(t) {
+  check('http', '{{domains[www2]}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, www subdomain #3, port #2');
+
+async_test(function(t) {
+  check('http', '{{domains[élève]}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, punycode subdomain #1, port #1');
+
+async_test(function(t) {
+  check('http', '{{domains[élève]}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, punycode subdomain #1, port #2');
+
+async_test(function(t) {
+  check('http', '{{domains[天気の良い日]}}', {{ports[http][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, punycode subdomain #2, port #1');
+
+async_test(function(t) {
+  check('http', '{{domains[天気の良い日]}}', {{ports[http][1]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTP protocol, punycode subdomain #2, port #2');
+
+async_test(function(t) {
+  check('https', '{{browser_host}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, no subdomain');
+
+async_test(function(t) {
+  check('https', '{{domains[www]}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, www subdomain #1');
+
+async_test(function(t) {
+  check('https', '{{domains[www1]}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, www subdomain #2');
+
+async_test(function(t) {
+  check('https', '{{domains[www2]}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, www subdomain #3');
+
+async_test(function(t) {
+  check('https', '{{domains[élève]}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, punycode subdomain #1');
+
+async_test(function(t) {
+  check('https', '{{domains[天気の良い日]}}', {{ports[https][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'HTTPS protocol, punycode subdomain #2');
+</script>
+</body>
+</html>
diff --git a/infrastructure/server/wpt-server-websocket.sub.html b/infrastructure/server/wpt-server-websocket.sub.html
new file mode 100644
index 0000000..ea7973a
--- /dev/null
+++ b/infrastructure/server/wpt-server-websocket.sub.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<html>
+  <head>
+    <title>WPT Server checker</title>
+    <meta charset="utf-8" />
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+  </head>
+</body>
+<body>
+<script>
+function check(protocol, domain, port, done) {
+  var url = protocol + '://' + domain + ':' + port + '/echo';
+  var ws = new WebSocket(url);
+
+  ws.addEventListener('error', function() {
+    done(false);
+  });
+
+  ws.addEventListener('open', function() {
+    done(true);
+  });
+}
+
+async_test(function(t) {
+  check('ws', '{{browser_host}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, no subdomain');
+
+async_test(function(t) {
+  check('ws', '{{domains[www1]}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, www subdomain #1');
+
+async_test(function(t) {
+  check('ws', '{{domains[www1]}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, www subdomain #2');
+
+async_test(function(t) {
+  check('ws', '{{domains[www2]}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, www subdomain #3');
+
+async_test(function(t) {
+  check('ws', '{{domains[élève]}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, punycode subdomain #1');
+
+async_test(function(t) {
+  check('ws', '{{domains[天気の良い日]}}', {{ports[ws][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WS protocol, punycode subdomain #2');
+
+async_test(function(t) {
+  check('wss', '{{browser_host}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, no subdomain');
+
+async_test(function(t) {
+  check('wss', '{{domains[www1]}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, www subdomain #1');
+
+async_test(function(t) {
+  check('wss', '{{domains[www1]}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, www subdomain #2');
+
+async_test(function(t) {
+  check('wss', '{{domains[www2]}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, www subdomain #3');
+
+async_test(function(t) {
+  check('wss', '{{domains[élève]}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, punycode subdomain #1');
+
+async_test(function(t) {
+  check('wss', '{{domains[天気の良い日]}}', {{ports[wss][0]}}, t.step_func(function(result) {
+    assert_true(result);
+
+    t.done();
+  }));
+}, 'WSS protocol, punycode subdomain #2');
+</script>
+</body>
+</html>
diff --git a/infrastructure/webdriver/tests/conftest.py b/infrastructure/webdriver/tests/conftest.py
new file mode 100644
index 0000000..cbc4f83
--- /dev/null
+++ b/infrastructure/webdriver/tests/conftest.py
@@ -0,0 +1,7 @@
+import os
+import sys
+# Hack to avoid duplicating the conftest file
+wdpath = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                      "../../../webdriver/"))
+sys.path.insert(0, wdpath)
+from tests.conftest import *
diff --git a/infrastructure/webdriver/tests/test_load_file.py b/infrastructure/webdriver/tests/test_load_file.py
new file mode 100644
index 0000000..370e63c
--- /dev/null
+++ b/infrastructure/webdriver/tests/test_load_file.py
@@ -0,0 +1,4 @@
+from tests.support.inline import inline
+
+def test_load(session):
+    inline("PASS")
diff --git a/innerText/getter-tests.js b/innerText/getter-tests.js
deleted file mode 100644
index 6df8912..0000000
--- a/innerText/getter-tests.js
+++ /dev/null
@@ -1,350 +0,0 @@
-testText("<div>abc", "abc", "Simplest possible test");
-
-/**** white-space:normal ****/
-
-testText("<div> abc", "abc", "Leading whitespace removed");
-testText("<div>abc ", "abc", "Trailing whitespace removed");
-testText("<div>abc  def", "abc def", "Internal whitespace compressed");
-testText("<div>abc\ndef", "abc def", "\\n converted to space");
-testText("<div>abc\rdef", "abc def", "\\r converted to space");
-testText("<div>abc\tdef", "abc def", "\\t converted to space");
-testText("<div>abc <br>def", "abc\ndef", "Trailing whitespace before hard line break removed");
-
-/**** <pre> ****/
-
-testText("<pre> abc", " abc", "Leading whitespace preserved");
-testText("<pre>abc ", "abc ", "Trailing whitespace preserved");
-testText("<pre>abc  def", "abc  def", "Internal whitespace preserved");
-testText("<pre>abc\ndef", "abc\ndef", "\\n preserved");
-testText("<pre>abc\rdef", "abc\ndef", "\\r converted to newline");
-testText("<pre>abc\tdef", "abc\tdef", "\\t preserved");
-testText("<div><pre>abc</pre><pre>def</pre>", "abc\ndef", "Two <pre> siblings");
-
-/**** <div style="white-space:pre"> ****/
-
-testText("<div style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
-testText("<div style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
-testText("<div style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
-testText("<div style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
-testText("<div style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
-testText("<div style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
-
-/**** <span style="white-space:pre"> ****/
-
-testText("<span style='white-space:pre'> abc", " abc", "Leading whitespace preserved");
-testText("<span style='white-space:pre'>abc ", "abc ", "Trailing whitespace preserved");
-testText("<span style='white-space:pre'>abc  def", "abc  def", "Internal whitespace preserved");
-testText("<span style='white-space:pre'>abc\ndef", "abc\ndef", "\\n preserved");
-testText("<span style='white-space:pre'>abc\rdef", "abc\ndef", "\\r converted to newline");
-testText("<span style='white-space:pre'>abc\tdef", "abc\tdef", "\\t preserved");
-
-/**** <div style="white-space:pre-line"> ****/
-
-testText("<div style='white-space:pre-line'> abc", "abc", "Leading whitespace removed");
-testText("<div style='white-space:pre-line'>abc ", "abc", "Trailing whitespace removed");
-testText("<div style='white-space:pre-line'>abc  def", "abc def", "Internal whitespace collapsed");
-testText("<div style='white-space:pre-line'>abc\ndef", "abc\ndef", "\\n preserved");
-testText("<div style='white-space:pre-line'>abc\rdef", "abc\ndef", "\\r converted to newline");
-testText("<div style='white-space:pre-line'>abc\tdef", "abc def", "\\t converted to space");
-
-/**** Collapsing whitespace across element boundaries ****/
-
-testText("<div><span>abc </span> def", "abc def", "Whitespace collapses across element boundaries");
-testText("<div><span>abc </span><span></span> def", "abc def", "Whitespace collapses across element boundaries");
-testText("<div><span>abc </span><span style='white-space:pre'></span> def", "abc def", "Whitespace collapses across element boundaries");
-
-/**** Soft line breaks ****/
-
-testText("<div style='width:0'>abc def", "abc def", "Soft line breaks ignored");
-
-/**** first-line/first-letter ****/
-
-testText("<div class='first-line-uppercase' style='width:0'>abc def", "ABC def", "::first-line styles applied");
-testText("<div class='first-letter-uppercase' style='width:0'>abc def", "Abc def", "::first-letter styles applied");
-testText("<div class='first-letter-float' style='width:0'>abc def", "abc def", "::first-letter float ignored");
-
-/**** &nbsp; ****/
-
-testText("<div>&nbsp;", "\xA0", "&nbsp; preserved");
-
-/**** display:none ****/
-
-testText("<div style='display:none'>abc", "abc", "display:none container");
-testText("<div style='display:none'>abc  def", "abc  def", "No whitespace compression in display:none container");
-testText("<div style='display:none'> abc def ", " abc def ", "No removal of leading/trailing whitespace in display:none container");
-testText("<div>123<span style='display:none'>abc", "123", "display:none child not rendered");
-testText("<div style='display:none'><span id='target'>abc", "abc", "display:none container with non-display-none target child");
-testTextInSVG("<div id='target'>abc", "", "non-display-none child of svg");
-testTextInSVG("<div style='display:none' id='target'>abc", "abc", "display:none child of svg");
-testTextInSVG("<div style='display:none'><div id='target'>abc", "abc", "child of display:none child of svg");
-
-/**** display:contents ****/
-
-if (CSS.supports("display", "contents")) {
-  testText("<div style='display:contents'>abc", "abc", "display:contents container");
-  testText("<div><div style='display:contents'>abc", "abc", "display:contents container");
-  testText("<div>123<span style='display:contents'>abc", "123abc", "display:contents rendered");
-  testText("<div style='display:contents'>   ", "", "display:contents not processed via textContent");
-  testText("<div><div style='display:contents'>   ", "", "display:contents not processed via textContent");
-}
-
-/**** visibility:hidden ****/
-
-testText("<div style='visibility:hidden'>abc", "", "visibility:hidden container");
-testText("<div>123<span style='visibility:hidden'>abc", "123", "visibility:hidden child not rendered");
-testText("<div style='visibility:hidden'>123<span style='visibility:visible'>abc", "abc", "visibility:visible child rendered");
-
-/**** visibility:collapse ****/
-
-testText("<table><tbody style='visibility:collapse'><tr><td>abc", "", "visibility:collapse row-group");
-testText("<table><tr style='visibility:collapse'><td>abc", "", "visibility:collapse row");
-testText("<table><tr><td style='visibility:collapse'>abc", "", "visibility:collapse cell");
-testText("<table><tbody style='visibility:collapse'><tr><td style='visibility:visible'>abc", "abc",
-         "visibility:collapse row-group with visible cell");
-testText("<table><tr style='visibility:collapse'><td style='visibility:visible'>abc", "abc",
-         "visibility:collapse row with visible cell");
-testText("<div style='display:flex'><span style='visibility:collapse'>1</span><span>2</span></div>",
-         "2", "visibility:collapse honored on flex item");
-testText("<div style='display:grid'><span style='visibility:collapse'>1</span><span>2</span></div>",
-         "2", "visibility:collapse honored on grid item");
-
-/**** opacity:0 ****/
-
-testText("<div style='opacity:0'>abc", "abc", "opacity:0 container");
-testText("<div style='opacity:0'>abc  def", "abc def", "Whitespace compression in opacity:0 container");
-testText("<div style='opacity:0'> abc def ", "abc def", "Remove leading/trailing whitespace in opacity:0 container");
-testText("<div>123<span style='opacity:0'>abc", "123abc", "opacity:0 child rendered");
-
-/**** generated content ****/
-
-testText("<div class='before'>", "", "Generated content not included");
-testText("<div><div class='before'>", "", "Generated content on child not included");
-
-/**** innerText on replaced elements ****/
-
-testText("<button>abc", "abc", "<button> contents preserved");
-testText("<fieldset>abc", "abc", "<fieldset> contents preserved");
-testText("<fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
-testText("<input type='text' value='abc'>", "", "<input> contents ignored");
-testText("<textarea>abc", "", "<textarea> contents ignored");
-testText("<iframe>abc", "", "<iframe> contents ignored");
-testText("<iframe><div id='target'>abc", "", "<iframe> contents ignored");
-testText("<iframe src='data:text/html,abc'>", "","<iframe> subdocument ignored");
-testText("<audio style='display:block'>abc", "", "<audio> contents ignored");
-testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "", "<audio> contents ignored");
-testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok if display:none");
-testText("<video>abc", "", "<video> contents ignored");
-testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "", "<video> contents ignored");
-testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok if display:none");
-testText("<canvas>abc", "", "<canvas> contents ignored");
-testText("<canvas><div id='target'>abc", "", "<canvas><div id='target'> contents ignored");
-testText("<img alt='abc'>", "", "<img> alt text ignored");
-testText("<img src='about:blank' class='poke'>", "", "<img> contents ignored");
-
-/**** <select>, <optgroup> & <option> ****/
-
-testText("<select size='1'><option>abc</option><option>def", "abc\ndef", "<select size='1'> contents of options preserved");
-testText("<select size='2'><option>abc</option><option>def", "abc\ndef", "<select size='2'> contents of options preserved");
-testText("<select size='1'><option id='target'>abc</option><option>def", "abc", "<select size='1'> contents of target option preserved");
-testText("<select size='2'><option id='target'>abc</option><option>def", "abc", "<select size='2'> contents of target option preserved");
-testText("<div>a<select></select>bc", "abc", "empty <select>");
-testText("<div>a<select><optgroup></select>bc", "a\nbc", "empty <optgroup> in <select>");
-testText("<div>a<select><option></select>bc", "a\nbc", "empty <option> in <select>");
-testText("<select class='poke'></select>", "", "<select> containing text node child");
-testText("<select><optgroup class='poke-optgroup'></select>", "", "<optgroup> containing <optgroup>");
-testText("<select><optgroup><option>abc</select>", "abc", "<optgroup> containing <option>");
-testText("<select><option class='poke-div'>123</select>", "123\nabc", "<div> in <option>");
-testText("<div>a<optgroup></optgroup>bc", "a\nbc", "empty <optgroup> in <div>");
-testText("<div>a<optgroup>123</optgroup>bc", "a\nbc", "<optgroup> in <div>");
-testText("<div>a<option></option>bc", "a\nbc", "empty <option> in <div>");
-testText("<div>a<option>123</option>bc", "a\n123\nbc", "<option> in <div>");
-
-/**** innerText on replaced element children ****/
-
-testText("<div><button>abc", "abc", "<button> contents preserved");
-testText("<div><fieldset>abc", "abc", "<fieldset> contents preserved");
-testText("<div><fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
-testText("<div><input type='text' value='abc'>", "", "<input> contents ignored");
-testText("<div><textarea>abc", "", "<textarea> contents ignored");
-testText("<div><select size='1'><option>abc</option><option>def", "abc\ndef", "<select size='1'> contents of options preserved");
-testText("<div><select size='2'><option>abc</option><option>def", "abc\ndef", "<select size='2'> contents of options preserved");
-testText("<div><iframe>abc", "", "<iframe> contents ignored");
-testText("<div><iframe src='data:text/html,abc'>", ""," <iframe> subdocument ignored");
-testText("<div><audio>abc", "", "<audio> contents ignored");
-testText("<div><video>abc", "", "<video> contents ignored");
-testText("<div><canvas>abc", "", "<canvas> contents ignored");
-testText("<div><img alt='abc'>", "", "<img> alt text ignored");
-
-/**** Lines around blocks ****/
-
-testText("<div>123<div>abc</div>def", "123\nabc\ndef", "Newline at block boundary");
-testText("<div>123<span style='display:block'>abc</span>def", "123\nabc\ndef", "Newline at display:block boundary");
-testText("<div>abc<div></div>def", "abc\ndef", "Empty block induces single line break");
-testText("<div>abc<div></div><div></div>def", "abc\ndef", "Consecutive empty blocks ignored");
-testText("<div><p>abc", "abc", "No blank lines around <p> alone");
-testText("<div><p>abc</p> ", "abc", "No blank lines around <p> followed by only collapsible whitespace");
-testText("<div> <p>abc</p>", "abc", "No blank lines around <p> preceded by only collapsible whitespace");
-testText("<div><p>abc<p>def", "abc\n\ndef", "Blank line between consecutive <p>s");
-testText("<div><p>abc</p> <p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by collapsible whitespace");
-testText("<div><p>abc</p><div></div><p>def", "abc\n\ndef", "Blank line between consecutive <p>s separated only by empty block");
-testText("<div><p>abc</p><div>123</div><p>def", "abc\n\n123\n\ndef", "Blank lines between <p>s separated by non-empty block");
-testText("<div>abc<div><p>123</p></div>def", "abc\n\n123\n\ndef", "Blank lines around a <p> in its own block");
-testText("<div>abc<p>def", "abc\n\ndef", "Blank line before <p>");
-testText("<div><p>abc</p>def", "abc\n\ndef", "Blank line after <p>");
-testText("<div><p>abc<p></p><p></p><p>def", "abc\n\ndef", "One blank line between <p>s, ignoring empty <p>s");
-testText("<div style='visibility:hidden'><p><span style='visibility:visible'>abc</span></p>\n<div style='visibility:visible'>def</div>",
-     "abc\ndef", "Invisible <p> doesn't induce extra line breaks");
-testText("<div>abc<div style='margin:2em'>def", "abc\ndef", "No blank lines around <div> with margin");
-testText("<div>123<span style='display:inline-block'>abc</span>def", "123abcdef", "No newlines at display:inline-block boundary");
-testText("<div>123<span style='display:inline-block'> abc </span>def", "123abcdef", "Leading/trailing space removal at display:inline-block boundary");
-testText("<div>123<p style='margin:0px'>abc</p>def", "123\n\nabc\n\ndef", "Blank lines around <p> even without margin");
-testText("<div>123<h1>abc</h1>def", "123\nabc\ndef", "No blank lines around <h1>");
-testText("<div>123<h2>abc</h2>def", "123\nabc\ndef", "No blank lines around <h2>");
-testText("<div>123<h3>abc</h3>def", "123\nabc\ndef", "No blank lines around <h3>");
-testText("<div>123<h4>abc</h4>def", "123\nabc\ndef", "No blank lines around <h4>");
-testText("<div>123<h5>abc</h5>def", "123\nabc\ndef", "No blank lines around <h5>");
-testText("<div>123<h6>abc</h6>def", "123\nabc\ndef", "No blank lines around <h6>");
-
-/**** Spans ****/
-
-testText("<div>123<span>abc</span>def", "123abcdef", "<span> boundaries are irrelevant");
-testText("<div>123 <span>abc</span> def", "123 abc def", "<span> boundaries are irrelevant");
-testText("<div style='width:0'>123 <span>abc</span> def", "123 abc def", "<span> boundaries are irrelevant");
-testText("<div>123<em>abc</em>def", "123abcdef", "<em> gets no special treatment");
-testText("<div>123<b>abc</b>def", "123abcdef", "<b> gets no special treatment");
-testText("<div>123<i>abc</i>def", "123abcdef", "<i> gets no special treatment");
-testText("<div>123<strong>abc</strong>def", "123abcdef", "<strong> gets no special treatment");
-testText("<div>123<tt>abc</tt>def", "123abcdef", "<tt> gets no special treatment");
-testText("<div>123<code>abc</code>def", "123abcdef", "<code> gets no special treatment");
-
-/**** Soft hyphen ****/
-
-testText("<div>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
-testText("<div style='width:0'>abc&shy;def", "abc\xADdef", "soft hyphen preserved");
-
-/**** Tables ****/
-
-testText("<div><table style='white-space:pre'>  <td>abc</td>  </table>", "abc", "Ignoring non-rendered table whitespace");
-testText("<div><table><tr><td>abc<td>def</table>", "abc\tdef", "Tab-separated table cells");
-testText("<div><table><tr><td>abc<td><td>def</table>", "abc\t\tdef", "Tab-separated table cells including empty cells");
-testText("<div><table><tr><td>abc<td><td></table>", "abc\t\t", "Tab-separated table cells including trailing empty cells");
-testText("<div><table><tr><td>abc<tr><td>def</table>", "abc\ndef", "Newline-separated table rows");
-testText("<div>abc<table><td>def</table>ghi", "abc\ndef\nghi", "Newlines around table");
-testText("<div><table style='border-collapse:collapse'><tr><td>abc<td>def</table>", "abc\tdef",
-         "Tab-separated table cells in a border-collapse table");
-testText("<div><table><tfoot>x</tfoot><tbody>y</tbody></table>", "xy", "tfoot not reordered");
-testText("<table><tfoot><tr><td>footer</tfoot><thead><tr><td style='visibility:collapse'>thead</thead><tbody><tr><td>tbody</tbody></table>",
-         "footer\n\ntbody", "");
-
-/**** Table captions ****/
-
-testText("<div><table><tr><td>abc<caption>def</caption></table>", "abc\ndef", "Newline between cells and caption");
-
-/**** display:table ****/
-
-testText("<div><div class='table'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>",
-         "abc\tdef", "Tab-separated table cells");
-testText("<div><div class='table'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
-         "abc\ndef", "Newline-separated table rows");
-testText("<div>abc<div class='table'><span class='cell'>def</span></div>ghi", "abc\ndef\nghi", "Newlines around table");
-
-/**** display:inline-table ****/
-
-testText("<div><div class='itable'><span class='cell'>abc</span>\n<span class='cell'>def</span></div>", "abc\tdef", "Tab-separated table cells");
-testText("<div><div class='itable'><span class='row'><span class='cell'>abc</span></span>\n<span class='row'><span class='cell'>def</span></span></div>",
-         "abc\ndef", "Newline-separated table rows");
-testText("<div>abc<div class='itable'><span class='cell'>def</span></div>ghi", "abcdefghi", "No newlines around inline-table");
-testText("<div>abc<div class='itable'><span class='row'><span class='cell'>def</span></span>\n<span class='row'><span class='cell'>123</span></span></div>ghi",
-         "abcdef\n123ghi", "Single newline in two-row inline-table");
-
-/**** Lists ****/
-
-testText("<div><ol><li>abc", "abc", "<ol> list items get no special treatment");
-testText("<div><ul><li>abc", "abc", "<ul> list items get no special treatment");
-
-/**** Misc elements ****/
-
-testText("<div><script style='display:block'>abc", "abc", "display:block <script> is rendered");
-testText("<div><style style='display:block'>abc", "abc", "display:block <style> is rendered");
-testText("<div><noscript style='display:block'>abc", "", "display:block <noscript> is not rendered (it's not parsed!)");
-testText("<div><template style='display:block'>abc", "",
-         "display:block <template> contents are not rendered (the contents are in a different document)");
-testText("<div>abc<br>def", "abc\ndef", "<br> induces line break");
-testText("<div>abc<br>", "abc\n", "<br> induces line break even at end of block");
-testText("<div><br class='poke'>", "\n", "<br> content ignored");
-testText("<div>abc<hr>def", "abc\ndef", "<hr> induces line break");
-testText("<div>abc<hr><hr>def", "abc\ndef", "<hr><hr> induces just one line break");
-testText("<div>abc<hr><hr><hr>def", "abc\ndef", "<hr><hr><hr> induces just one line break");
-testText("<div><hr class='poke'>", "abc", "<hr> content rendered");
-testText("<div>abc<!--comment-->def", "abcdef", "comment ignored");
-
-/**** text-transform ****/
-
-testText("<div><div style='text-transform:uppercase'>abc", "ABC", "text-transform is applied");
-testText("<div><div style='text-transform:uppercase'>Ma\xDF", "MASS", "text-transform handles es-zet");
-testText("<div><div lang='tr' style='text-transform:uppercase'>i \u0131", "\u0130 I", "text-transform handles Turkish casing");
-
-/**** block-in-inline ****/
-
-testText("<div>abc<span>123<div>456</div>789</span>def", "abc123\n456\n789def", "block-in-inline doesn't add unnecessary newlines");
-
-/**** floats ****/
-
-testText("<div>abc<div style='float:left'>123</div>def", "abc\n123\ndef", "floats induce a block boundary");
-testText("<div>abc<span style='float:left'>123</span>def", "abc\n123\ndef", "floats induce a block boundary");
-
-/**** position ****/
-
-testText("<div>abc<div style='position:absolute'>123</div>def", "abc\n123\ndef", "position:absolute induces a block boundary");
-testText("<div>abc<span style='position:absolute'>123</span>def", "abc\n123\ndef", "position:absolute induces a block boundary");
-testText("<div>abc<div style='position:relative'>123</div>def", "abc\n123\ndef", "position:relative has no effect");
-testText("<div>abc<span style='position:relative'>123</span>def", "abc123def", "position:relative has no effect");
-
-/**** text-overflow:ellipsis ****/
-
-testText("<div style='overflow:hidden'>abc", "abc", "overflow:hidden ignored");
-// XXX Chrome skips content with width:0 or height:0 and overflow:hidden;
-// should we spec that?
-testText("<div style='width:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero width");
-testText("<div style='height:0; overflow:hidden'>abc", "abc", "overflow:hidden ignored even with zero height");
-testText("<div style='width:0; overflow:hidden; text-overflow:ellipsis'>abc", "abc", "text-overflow:ellipsis ignored");
-
-/**** Support on non-HTML elements ****/
-
-testText("<svg>abc", undefined, "innerText not supported on SVG elements");
-testText("<math>abc", undefined, "innerText not supported on MathML elements");
-
-/**** Ruby ****/
-
-testText("<div><ruby>abc<rt>def</rt></ruby>", "abcdef", "<rt> and no <rp>");
-testText("<div><ruby>abc<rp>(</rp><rt>def</rt><rp>)</rp></ruby>", "abcdef", "<rp>");
-testText("<div><rp>abc</rp>", "", "Lone <rp>");
-testText("<div><rp style='visibility:hidden'>abc</rp>", "", "visibility:hidden <rp>");
-testText("<div><rp style='display:block'>abc</rp>def", "abc\ndef", "display:block <rp>");
-testText("<div><rp style='display:block'> abc </rp>def", "abc\ndef", "display:block <rp> with whitespace");
-testText("<div><select class='poke-rp'></select>", "", "<rp> in a <select>");
-
-/**** Shadow DOM ****/
-
-if ("createShadowRoot" in document.body) {
-  testText("<div class='shadow'>", "", "Shadow DOM contents ignored");
-  testText("<div><div class='shadow'>", "", "Shadow DOM contents ignored");
-}
-
-/**** Flexbox ****/
-
-if (CSS.supports('display', 'flex')) {
-  testText("<div style='display:flex'><div style='order:1'>1</div><div>2</div></div>",
-           "1\n2", "CSS 'order' property ignored");
-  testText("<div style='display:flex'><span>1</span><span>2</span></div>",
-           "1\n2", "Flex items blockified");
-}
-
-/**** Grid ****/
-
-if (CSS.supports('display', 'grid')) {
-  testText("<div style='display:grid'><div style='order:1'>1</div><div>2</div></div>",
-           "1\n2", "CSS 'order' property ignored");
-  testText("<div style='display:grid'><span>1</span><span>2</span></div>",
-           "1\n2", "Grid items blockified");
-}
diff --git a/innerText/getter.html b/innerText/getter.html
deleted file mode 100644
index 7a517db..0000000
--- a/innerText/getter.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<title>innerText getter test</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<style>
-.before::before { content:'abc'; }
-.table { display:table; }
-.itable { display:inline-table; }
-.row { display:table-row; }
-.cell { display:table-cell; }
-.first-line-uppercase::first-line { text-transform:uppercase; }
-.first-letter-uppercase::first-letter { text-transform:uppercase; }
-.first-letter-float::first-letter { float:left; }
-</style>
-<div id="container"></div>
-<svg id="svgContainer"></svg>
-<script>
-let container = document.querySelector('#container');
-let svgContainer = document.querySelector('#svgContainer');
-function testText(html, expectedPlain, msg) {
-  textTextInContainer(container, html, expectedPlain, msg);
-}
-function testTextInSVG(html, expectedPlain, msg) {
-  textTextInContainer(svgContainer, html, expectedPlain, msg);
-}
-function textTextInContainer(cont, html, expectedPlain, msg) {
-  test(function() {
-    container.innerHTML = html;
-    if (cont != container) {
-      while (container.firstChild) {
-        cont.appendChild(container.firstChild);
-      }
-    }
-    var e = document.getElementById('target');
-    if (!e) {
-      e = cont.firstChild;
-    }
-    var pokes = document.getElementsByClassName('poke');
-    for (var i = 0; i < pokes.length; ++i) {
-      pokes[i].textContent = 'abc';
-    }
-    ['rp', 'optgroup', 'div'].forEach(function(tag) {
-      pokes = document.getElementsByClassName('poke-' + tag);
-      for (var i = 0; i < pokes.length; ++i) {
-        var el = document.createElement(tag);
-        el.textContent = "abc";
-        pokes[i].appendChild(el);
-      }
-    });
-    var shadows = document.getElementsByClassName('shadow');
-    for (var i = 0; i < shadows.length; ++i) {
-      var s = shadows[i].createShadowRoot();
-      s.textContent = 'abc';
-    }
-    while (e && e.nodeType != Node.ELEMENT_NODE) {
-      e = e.nextSibling;
-    }
-    assert_equals(e.innerText, expectedPlain);
-    cont.textContent = '';
-  }, msg + ' (' + format_value(html) + ')');
-}
-</script>
-<script src="getter-tests.js"></script>
diff --git a/interfaces/cssom.idl b/interfaces/cssom.idl
index 708c6fc..bbbd927 100644
--- a/interfaces/cssom.idl
+++ b/interfaces/cssom.idl
@@ -4,8 +4,7 @@
 
 typedef USVString CSSOMString;
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface MediaList {
   stringifier attribute [TreatNullAs=EmptyString] CSSOMString mediaText;
   readonly attribute unsigned long length;
@@ -33,8 +32,7 @@
   void deleteRule(unsigned long index);
 };
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface StyleSheetList {
   getter StyleSheet? item(unsigned long index);
   readonly attribute unsigned long length;
@@ -52,8 +50,7 @@
 
 ProcessingInstruction implements LinkStyle;
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface CSSRuleList {
   getter CSSRule? item(unsigned long index);
   readonly attribute unsigned long length;
diff --git a/interfaces/dom.idl b/interfaces/dom.idl
index 087fe4d..45cae33 100644
--- a/interfaces/dom.idl
+++ b/interfaces/dom.idl
@@ -5,6 +5,7 @@
   readonly attribute EventTarget? target;
   readonly attribute EventTarget? srcElement;
   readonly attribute EventTarget? currentTarget;
+  sequence<EventTarget> composedPath();
 
   const unsigned short NONE = 0;
   const unsigned short CAPTURING_PHASE = 1;
@@ -20,6 +21,7 @@
            attribute boolean returnValue;
   void preventDefault();
   readonly attribute boolean defaultPrevented;
+  readonly attribute boolean composed;
 
   [Unforgeable] readonly attribute boolean isTrusted;
   readonly attribute DOMTimeStamp timeStamp;
diff --git a/interfaces/fetch.idl b/interfaces/fetch.idl
new file mode 100644
index 0000000..346d1b0
--- /dev/null
+++ b/interfaces/fetch.idl
@@ -0,0 +1,97 @@
+typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;
+
+[Constructor(optional HeadersInit init),
+ Exposed=(Window,Worker)]
+interface Headers {
+  void append(ByteString name, ByteString value);
+  void delete(ByteString name);
+  ByteString? get(ByteString name);
+  boolean has(ByteString name);
+  void set(ByteString name, ByteString value);
+  iterable<ByteString, ByteString>;
+};
+typedef (Blob or BufferSource or FormData or URLSearchParams or ReadableStream or USVString) BodyInit;
+interface mixin Body {
+  readonly attribute ReadableStream? body;
+  readonly attribute boolean bodyUsed;
+  [NewObject] Promise<ArrayBuffer> arrayBuffer();
+  [NewObject] Promise<Blob> blob();
+  [NewObject] Promise<FormData> formData();
+  [NewObject] Promise<any> json();
+  [NewObject] Promise<USVString> text();
+};
+typedef (Request or USVString) RequestInfo;
+
+[Constructor(RequestInfo input, optional RequestInit init),
+ Exposed=(Window,Worker)]
+interface Request {
+  readonly attribute ByteString method;
+  readonly attribute USVString url;
+  [SameObject] readonly attribute Headers headers;
+
+  readonly attribute RequestDestination destination;
+  readonly attribute USVString referrer;
+  readonly attribute ReferrerPolicy referrerPolicy;
+  readonly attribute RequestMode mode;
+  readonly attribute RequestCredentials credentials;
+  readonly attribute RequestCache cache;
+  readonly attribute RequestRedirect redirect;
+  readonly attribute DOMString integrity;
+  readonly attribute boolean keepalive;
+  readonly attribute boolean isReloadNavigation;
+  readonly attribute AbortSignal signal;
+
+  [NewObject] Request clone();
+};
+Request includes Body;
+
+dictionary RequestInit {
+  ByteString method;
+  HeadersInit headers;
+  BodyInit? body;
+  USVString referrer;
+  ReferrerPolicy referrerPolicy;
+  RequestMode mode;
+  RequestCredentials credentials;
+  RequestCache cache;
+  RequestRedirect redirect;
+  DOMString integrity;
+  boolean keepalive;
+  AbortSignal? signal;
+  any window; // can only be set to null
+};
+
+enum RequestDestination { "", "audio", "audioworklet", "document", "embed", "font", "image", "manifest", "object", "paintworklet", "report", "script", "sharedworker", "style",  "track", "video", "worker", "xslt" };
+enum RequestMode { "navigate", "same-origin", "no-cors", "cors" };
+enum RequestCredentials { "omit", "same-origin", "include" };
+enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" };
+enum RequestRedirect { "follow", "error", "manual" };
+[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]
+interface Response {
+  [NewObject] static Response error();
+  [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
+
+  readonly attribute ResponseType type;
+
+  readonly attribute USVString url;
+  readonly attribute boolean redirected;
+  readonly attribute unsigned short status;
+  readonly attribute boolean ok;
+  readonly attribute ByteString statusText;
+  [SameObject] readonly attribute Headers headers;
+  readonly attribute Promise<Headers> trailer;
+
+  [NewObject] Response clone();
+};
+Response includes Body;
+
+dictionary ResponseInit {
+  unsigned short status = 200;
+  ByteString statusText = "OK";
+  HeadersInit headers;
+};
+
+enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" };
+partial interface mixin WindowOrWorkerGlobalScope {
+  [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
+};
diff --git a/interfaces/geometry.idl b/interfaces/geometry.idl
index 7ee2a5a..be1d56b 100644
--- a/interfaces/geometry.idl
+++ b/interfaces/geometry.idl
@@ -80,7 +80,6 @@
     unrestricted double height = 0;
 };
 
-[LegacyArrayClass]
 interface DOMRectList {
     readonly attribute unsigned long length;
     getter DOMRect? item(unsigned long index);
diff --git a/interfaces/html.idl b/interfaces/html.idl
index af85b42..8552ae4 100644
--- a/interfaces/html.idl
+++ b/interfaces/html.idl
@@ -1437,6 +1437,7 @@
   [CEReactions] void define(DOMString name, Function constructor, optional ElementDefinitionOptions options);
   any get(DOMString name);
   Promise<void> whenDefined(DOMString name);
+  [CEReactions] void upgrade(Node root);
 };
 
 dictionary ElementDefinitionOptions {
diff --git a/interfaces/web-audio-api.idl b/interfaces/web-audio-api.idl
index 2514dad..9c25365 100644
--- a/interfaces/web-audio-api.idl
+++ b/interfaces/web-audio-api.idl
@@ -167,9 +167,15 @@
   ChannelInterpretation channelInterpretation;
 };
 
+enum AutomationRate {
+  "a-rate",
+  "k-rate"
+};
+
 [Exposed=Window]
 interface AudioParam {
   attribute float value;
+  attribute AutomationRate automationRate;
   readonly attribute float defaultValue;
   readonly attribute float minValue;
   readonly attribute float maxValue;
diff --git a/intersection-observer/shadow-content.html b/intersection-observer/shadow-content.html
index a0a6242..d049c70 100644
--- a/intersection-observer/shadow-content.html
+++ b/intersection-observer/shadow-content.html
@@ -23,7 +23,7 @@
 runTestCycle(function() {
   var shadowHost = document.getElementById("host");
   assert_true(!!shadowHost, "Host exists");
-  var shadowRoot = shadowHost.createShadowRoot();
+  var shadowRoot = shadowHost.attachShadow({ mode: "open" });
   assert_true(!!shadowRoot, "Shadow root exists");
   shadowRoot.innerHTML = "<div id='target' style='width: 100px; height: 100px; background-color: green;'></div>";
   target = shadowRoot.getElementById("target");
diff --git a/js/OWNERS b/js/OWNERS
new file mode 100644
index 0000000..fd31fb2
--- /dev/null
+++ b/js/OWNERS
@@ -0,0 +1 @@
+@Ms2ger
diff --git a/keyboard-lock/navigator-keyboard-lock-blocked-from-iframe.https.html b/keyboard-lock/navigator-keyboard-lock-blocked-from-iframe.https.html
new file mode 100644
index 0000000..a01f6a2
--- /dev/null
+++ b/keyboard-lock/navigator-keyboard-lock-blocked-from-iframe.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+'use strict';
+
+promise_test(() => {
+  let iframe = document.createElement('iframe');
+  iframe.src = 'resources/iframe-lock-helper.html';
+  iframe.onload = () => {
+    iframe.contentWindow.postMessage('Ready', '*');
+  }
+
+  document.body.appendChild(iframe);
+
+  return new Promise((resolve,reject) => {
+    window.onmessage = message => {
+      if (message.data == 'Success') {
+        resolve();
+      } else if (message.data == 'Failure') {
+        reject();
+      }
+    }
+  });
+}, '[Keyboard Lock] navigator.keyboard.lock blocked from within iframe');
+
+</script>
diff --git a/keyboard-lock/resources/iframe-lock-helper.html b/keyboard-lock/resources/iframe-lock-helper.html
new file mode 100644
index 0000000..3950802
--- /dev/null
+++ b/keyboard-lock/resources/iframe-lock-helper.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script>
+'use strict';
+
+window.onmessage = message => {
+  if (message.data === 'Ready') {
+    let onSuccess = () => { parent.postMessage('Failure', '*'); };
+    let onError = error => {
+      if (error.name == 'InvalidStateError') {
+        parent.postMessage('Success', '*');
+      } else {
+        parent.postMessage('Failure', '*');
+      }
+    };
+
+    navigator.keyboard.lock().then(onSuccess, onError);
+  }
+};
+
+</script>
diff --git a/lint.whitelist b/lint.whitelist
index bc38595..e7e1af8 100644
--- a/lint.whitelist
+++ b/lint.whitelist
@@ -309,6 +309,7 @@
 SET TIMEOUT: css/css-fonts/font-display/font-display.html
 SET TIMEOUT: css/css-fonts/font-display/font-display-change.html
 SET TIMEOUT: css/css-fonts/font-display/font-display-change-ref.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-preload.html
 SET TIMEOUT: html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html
 SET TIMEOUT: html/dom/documents/dom-tree-accessors/Document.currentScript.html
 SET TIMEOUT: html/webappapis/timers/*
@@ -779,6 +780,8 @@
 CSS-COLLIDING-REF-NAME: css/CSS2/linebox/inline-formatting-context-001-ref.xht
 CSS-COLLIDING-REF-NAME: css/css-transforms/individual-transform/individual-transform-1-ref.html
 CSS-COLLIDING-REF-NAME: css/vendor-imports/mozilla/mozilla-central-reftests/transforms/individual-transform-1-ref.html
+CSS-COLLIDING-REF-NAME: css/css-flexbox/reference/percentage-size-subitems-001-ref.html
+CSS-COLLIDING-REF-NAME: css/css-grid/grid-items/percentage-size-subitems-001-ref.html
 CSS-COLLIDING-SUPPORT-NAME: css/css-backgrounds/support/red.png
 CSS-COLLIDING-SUPPORT-NAME: css/compositing/mix-blend-mode/support/red.png
 CSS-COLLIDING-SUPPORT-NAME: css/compositing/background-blending/support/red.png
@@ -797,7 +800,6 @@
 CSS-COLLIDING-SUPPORT-NAME: css/vendor-imports/mozilla/mozilla-central-reftests/ui3/support/replaced-min-max-1.png
 
 # CSS tests that used to be at the top level and weren't subject to lints
-MISSING-LINK: css/css-fonts/font-display/font-display.html
 MISSING-LINK: css/css-fonts/matching/fixed-stretch-style-over-weight.html
 SUPPORT-WRONG-DIR: css/css-fonts/matching/font-matching.css
 MISSING-LINK: css/css-fonts/matching/stretch-distance-over-weight-distance.html
diff --git a/mediacapture-streams/MediaStream-MediaElement-preload-none-manual.https.html b/mediacapture-streams/MediaStream-MediaElement-preload-none-manual.https.html
new file mode 100644
index 0000000..758a273
--- /dev/null
+++ b/mediacapture-streams/MediaStream-MediaElement-preload-none-manual.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Test that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</title>
+        <link rel="author" title="Matthew Wolenetz" href="mailto:wolenetz@chromium.org"/>
+        <script src="/resources/testharness.js"></script>
+        <script src="/resources/testharnessreport.js"></script>
+    </head>
+    <body>
+        <p class="instructions">When prompted, accept to share your audio and video streams.</p>
+        <p class="instructions">This test checks that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</p>
+        <div id=log></div>
+
+        <audio preload="none"></audio>
+        <video preload="none"></video>
+
+        <script>
+            function testPreloadNone(t, mediaElement, setSourceStreamFunc)
+            {
+                // The optional deferred load steps (for preload none) for MediaStream resources should be skipped.
+                mediaElement.addEventListener("suspend", t.unreached_func("'suspend' should not be fired."));
+                mediaElement.addEventListener("error", t.step_func(function() {
+                  assert_unreached("'error' should not be fired, code=" + mediaElement.error.code);
+                }));
+
+                mediaElement.addEventListener("loadeddata", t.step_func(function()
+                {
+                    assert_equals(mediaElement.networkState, mediaElement.NETWORK_LOADING);
+                    t.done();
+                }));
+
+                setSourceStreamFunc();
+                assert_equals(mediaElement.networkState, mediaElement.NETWORK_NO_SOURCE); // Resource selection is active.
+            }
+
+            async_test(function(t)
+            {
+                var aud = document.querySelector("audio");
+                navigator.mediaDevices.getUserMedia({audio:true})
+                  .then(t.step_func(function(stream)
+                  {
+                      testPreloadNone(t, aud, t.step_func(function() { aud.srcObject = stream; }));
+                  }),
+                  t.unreached_func("getUserMedia error callback was invoked."));
+            }, "Test that preload 'none' is ignored for MediaStream object URL used as srcObject for audio");
+
+            async_test(function(t)
+            {
+                var vid = document.querySelector("video");
+                navigator.mediaDevices.getUserMedia({video:true})
+                  .then(t.step_func(function(stream)
+                  {
+                      testPreloadNone(t, vid, t.step_func(function() { vid.srcObject = stream; }));
+                  }), t.unreached_func("getUserMedia error callback was invoked."));
+            }, "Test that preload 'none' is ignored for MediaStream used as srcObject for video");
+        </script>
+    </body>
+</html>
diff --git a/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html b/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html
deleted file mode 100644
index 6015bb7..0000000
--- a/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). -->
-<html>
-    <head>
-        <title>Test that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</title>>
-        <link rel="author" title="Matthew Wolenetz" href="mailto:wolenetz@chromium.org"/>
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-    </head>
-    <body>
-        <p class="instructions">When prompted, accept to share your audio and video streams.</p>
-        <h1 class="instructions">Description</h1>
-        <p class="instructions">This test checks that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</p>
-
-        <audio preload="none"></audio>
-        <video preload="none"></video>
-
-        <script>
-            function testPreloadNone(t, mediaElement, setSourceStreamFunc)
-            {
-                // The optional deferred load steps (for preload none) for MediaStream resources should be skipped.
-                mediaElement.addEventListener("suspend", t.unreached_func("'suspend' should not be fired."));
-                mediaElement.addEventListener("error", t.step_func(function() {
-                  assert_unreached("'error' should not be fired, code=" + mediaElement.error.code);
-                }));
-
-                mediaElement.addEventListener("loadeddata", t.step_func(function()
-                {
-                    assert_equals(mediaElement.networkState, mediaElement.NETWORK_LOADING);
-                    t.done();
-                }));
-
-                setSourceStreamFunc();
-                assert_equals(mediaElement.networkState, mediaElement.NETWORK_NO_SOURCE); // Resource selection is active.
-            }
-
-            async_test(function(t)
-            {
-                var aud = document.querySelector("audio");
-                navigator.mediaDevices.getUserMedia({audio:true})
-                  .then(t.step_func(function(stream)
-                  {
-                      testPreloadNone(t, aud, t.step_func(function()
-                      {
-                          aud.src = URL.createObjectURL(stream);
-                          t.add_cleanup(function() { URL.revokeObjectURL(aud.src); });
-                      }));
-                  }),
-                  t.unreached_func("getUserMedia error callback was invoked."));
-            }, "Test that preload 'none' is ignored for MediaStream object URL used as src");
-
-            async_test(function(t)
-            {
-                var vid = document.querySelector("video");
-                navigator.mediaDevices.getUserMedia({video:true})
-                  .then(t.step_func(function(stream)
-                  {
-                      testPreloadNone(t, vid, t.step_func(function() { vid.srcObject = stream; }));
-                  }), t.unreached_func("getUserMedia error callback was invoked."));
-            }, "Test that preload 'none' is ignored for MediaStream used as srcObject");
-        </script>
-    </body>
-</html>
diff --git a/mediacapture-streams/historical.html b/mediacapture-streams/historical.html
index d9616f1..74efc75 100644
--- a/mediacapture-streams/historical.html
+++ b/mediacapture-streams/historical.html
@@ -15,4 +15,9 @@
 test(function() {
   assert_false("mozGetUserMedia" in navigator);
 }, "navigator.mozGetUserMedia should not exist");
+
+test(() => {
+  const mediaStream = new MediaStream();
+  assert_throws(new TypeError(), () => URL.createObjectURL(mediaStream));
+}, "Passing MediaStream to URL.createObjectURL() should throw");
 </script>
diff --git a/mimesniff/mime-types/resources/mime-types.json b/mimesniff/mime-types/resources/mime-types.json
index 3dcdf55..1d0b152 100644
--- a/mimesniff/mime-types/resources/mime-types.json
+++ b/mimesniff/mime-types/resources/mime-types.json
@@ -272,6 +272,10 @@
     "output": null
   },
   {
+    "input": "/",
+    "output": null
+  },
+  {
     "input": "bogus",
     "output": null
   },
@@ -296,6 +300,10 @@
     "output": null
   },
   {
+    "input": "ÿ/ÿ",
+    "output": null
+  },
+  {
     "input": "text/html(;doesnot=matter",
     "output": null
   },
diff --git a/navigation-timing/resources/performance_attribute_sender.html b/navigation-timing/resources/performance_attribute_sender.html
new file mode 100644
index 0000000..99f5446
--- /dev/null
+++ b/navigation-timing/resources/performance_attribute_sender.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<script>
+parent.postMessage([
+  performance.timing.connectStart,
+  performance.timing.navigationStart,
+  performance.timing.secureConnectionStart,
+  performance.timing.connectEnd
+], '*');
+</script>
+</body>
+</html>
diff --git a/navigation-timing/test_performance_attributes.sub.html b/navigation-timing/test_performance_attributes.sub.html
new file mode 100644
index 0000000..4c9ce6e
--- /dev/null
+++ b/navigation-timing/test_performance_attributes.sub.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Use https to ensure secureConnectionStart is nontrivial, if it is supported. -->
+<iframe src="https://{{host}}:{{ports[https][0]}}/navigation-timing/resources/performance_attribute_sender.html"></iframe>
+<body>
+<script>
+async_test(function(t) {
+  window.addEventListener('message', t.step_func_done(function(event) {
+    const connectStart = event.data[0];
+    const navigationStart = event.data[1];
+    const secureConnectionStart = event.data[2];
+    const connectEnd = event.data[3];
+    assert_greater_than_equal(connectStart, navigationStart,
+      'performance.timing.connectStart >= performance.timing.navigationStart');
+    // secureConnectionStart is an optional attribute.
+    if (secureConnectionStart == undefined) {
+      return;
+    }
+    assert_greater_than_equal(secureConnectionStart, connectStart,
+      'performance.timing.secureConnectionStart >= performance.timing.connectStart');
+    assert_greater_than_equal(connectEnd, secureConnectionStart,
+      'performance.timing.connectEnd >= performance.timing.secureConnectionStart');
+  }));
+}, 'Check that performance.timing has reasonable values for secureConnectionStart and other attributes');
+</script>
+</body>
+</html>
diff --git a/offscreen-canvas/the-offscreen-canvas/offscreencanvas.commit.w.html b/offscreen-canvas/the-offscreen-canvas/offscreencanvas.commit.w.html
index 3268021..380686f 100644
--- a/offscreen-canvas/the-offscreen-canvas/offscreencanvas.commit.w.html
+++ b/offscreen-canvas/the-offscreen-canvas/offscreencanvas.commit.w.html
@@ -8,10 +8,15 @@
 
 function testCommitPushesContents(offscreenCanvas)
 {
+  try {
     var ctx = offscreenCanvas.getContext('2d');
     ctx.fillStyle = "#0f0";
     ctx.fillRect(0, 0, 10, 10);
     ctx.commit();
+    return true;
+  } catch(e) {
+    return false;
+  }
 }
 
 function isInvalidStateError(funcStr, ctx)
@@ -37,8 +42,7 @@
 self.onmessage = function(e) {
     switch (e.data.msg) {
         case 'test1':
-            testCommitPushesContents(e.data.data);
-            self.postMessage('worker finished');
+            self.postMessage(testCommitPushesContents(e.data.data));
             break;
         case 'test2':
             self.postMessage(testCommitException());
@@ -71,7 +75,8 @@
     var offscreenCanvas = placeholder.transferControlToOffscreen();
     var worker = makeWorker(document.getElementById("myWorker").textContent);
     worker.addEventListener('message', t.step_func_done(function(msg) {
-        verifyPlaceholder(placeholder);
+      assert_true(msg.data);
+      verifyPlaceholder(placeholder);
     }));
     worker.postMessage({msg: 'test1', data: offscreenCanvas}, [offscreenCanvas]);
 }, "Test that calling OffscreenCanvas's commit pushes its contents to its placeholder.");
@@ -85,4 +90,3 @@
 }, "Test that calling commit on an OffscreenCanvas that is not transferred from a HTMLCanvasElement throws an exception in a worker.");
 
 </script>
-
diff --git a/old-tests/OWNERS b/old-tests/OWNERS
new file mode 100644
index 0000000..04a7557
--- /dev/null
+++ b/old-tests/OWNERS
@@ -0,0 +1,2 @@
+@foolip
+@Ms2ger
diff --git a/old-tests/submission/migration.txt b/old-tests/submission/migration.txt
deleted file mode 100644
index 9480d75..0000000
--- a/old-tests/submission/migration.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-
-TODO:
-
-    Google          (not sure how much of that is what's already in)
-    Infraware       (these come bundled with resources and require lots of rewriting)
-    Intel           (need to check how much overlap there is with existing stuff)
-    Microsoft       (lots of things, need to check overlap)
-    Ms2ger          (Ms2ger is doing those)
-    Opera           (split into multiple PRs)
-    TestTWF_Paris
-    W3C
-
-DONE:
-
-    Apple
-    AryehGregor
-    Baidu
-    Comcast
-    DavidCarlisle
-    html5bidi
-    MathiasBynens
-    PhilipTaylor
-    MOSQUITO
-    Mozilla
-    WebKit
-    
\ No newline at end of file
diff --git a/payment-handler/same-object-attributes.https.html b/payment-handler/same-object-attributes.https.html
new file mode 100644
index 0000000..b9a9dd8
--- /dev/null
+++ b/payment-handler/same-object-attributes.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="help" href="https://w3c.github.io/payment-handler/">
+<title>Test for [SameObject] attributes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  const registration = await service_worker_unregister_and_register(
+      t, 'basic-card.js', 'payment-app/');
+  await wait_for_state(t, registration.installing, 'activated');
+
+  assert_equals(registration.paymentManager, registration.paymentManager);
+  assert_equals(registration.paymentManager.instruments, registration.paymentManager.instruments);
+});
+
+</script>
diff --git a/payment-handler/untrusted-event.https.html b/payment-handler/untrusted-event.https.html
new file mode 100644
index 0000000..900ac79
--- /dev/null
+++ b/payment-handler/untrusted-event.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="help" href="https://w3c.github.io/payment-handler/">
+<title>Test for untrusted event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+async function getResultFromSW(serviceWorkerContainer) {
+  return new Promise((resolve, reject) => {
+    serviceWorkerContainer.addEventListener('message', listener = e => {
+      serviceWorkerContainer.removeEventListener('message', listener);
+      if (e.data) {
+        resolve(e.data);
+      } else {
+        reject();
+      }
+    });
+  });
+}
+
+promise_test(async t => {
+  const registration = await service_worker_unregister_and_register(
+      t, 'untrusted-event.js', 'payment-app/');
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const controlled_window = (await with_iframe('payment-app/payment.html')).contentWindow;
+
+  // Test for untrusted PaymentRequestEvent
+  {
+    const result = getResultFromSW(controlled_window.navigator.serviceWorker);
+    controlled_window.navigator.serviceWorker.controller.postMessage('paymentrequest');
+
+    const expected = [
+      "InvalidStateError", /* respondWith */
+      "InvalidStateError"  /* openWindow */
+    ];
+
+    assert_array_equals(await result, expected);
+  }
+
+  // Test for untrusted CanMakePaymentEvent
+  {
+    const result = getResultFromSW(controlled_window.navigator.serviceWorker);
+    controlled_window.navigator.serviceWorker.controller.postMessage('canmakepayment');
+
+    const expected = [
+      "InvalidStateError", /* respondWith */
+    ];
+
+    assert_array_equals(await result, expected);
+  }
+});
+
+</script>
diff --git a/payment-handler/untrusted-event.js b/payment-handler/untrusted-event.js
new file mode 100644
index 0000000..2407029
--- /dev/null
+++ b/payment-handler/untrusted-event.js
@@ -0,0 +1,59 @@
+let sender = null;
+
+self.addEventListener('message', e => {
+  sender = e.source;
+
+  if (e.data == 'paymentrequest') {
+    self.dispatchEvent(new PaymentRequestEvent('paymentrequest', {
+      methodData: [{
+        supportedMethods: 'basic-card'
+      }],
+      total: {
+        currency: 'USD',
+        value: '100'
+      },
+      modifiers: [{
+        supportedMethods: 'basic-card'
+      }]
+    }));
+  } else if (e.data == 'canmakepayment') {
+    self.dispatchEvent(new CanMakePaymentEvent('canmakepayment', {
+      methodData: [{
+        supportedMethods: 'basic-card'
+      }],
+      modifiers: [{
+        supportedMethods: 'basic-card'
+      }]
+    }));
+  }
+});
+
+self.addEventListener('paymentrequest', async e => {
+  const result = [];
+
+  try {
+    e.respondWith({});
+  } catch (exception) {
+    result.push(exception.name);
+  }
+
+  try {
+    await e.openWindow('payment-app/payment.html');
+  } catch (exception) {
+    result.push(exception.name);
+  }
+
+  sender.postMessage(result);
+});
+
+self.addEventListener('canmakepayment', async e => {
+  const result = [];
+
+  try {
+    e.respondWith({});
+  } catch (exception) {
+    result.push(exception.name);
+  }
+
+  sender.postMessage(result);
+});
diff --git a/payment-request/PaymentCurrencyAmount/currencySystem-member.https.html b/payment-request/PaymentCurrencyAmount/currencySystem-member.https.html
deleted file mode 100644
index c79fb0f..0000000
--- a/payment-request/PaymentCurrencyAmount/currencySystem-member.https.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Tests for PaymentCurrencyAmount's currencySystem</title>
-<link rel="help" href="https://www.w3.org/TR/payment-request/#dom-paymentcurrencyamount-currencysystem">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-test(() => {
-  const validAmount = {
-    value: "0",
-    currency: "USD",
-    currencySystem: "urn:iso:std:iso:4217",
-  }
-  const validMethods = [{ supportedMethods: "valid-method" }];
-  const validDetails = {
-    total: {
-      label: "",
-      amount: validAmount,
-    },
-  };
-  // smoke test
-  const request = new PaymentRequest(validMethods, validDetails);
-
-  // real test
-  assert_throws(new TypeError(), () => {
-    const invalidAmount = {
-      ...validAmount,
-      currencySystem: "this will cause the TypeError"
-    }
-    const invalidDetails = {
-      total: {
-        label: "",
-        amount: invalidAmount,
-      },
-    };
-    const request = new PaymentRequest(validMethods, invalidDetails);
-  })
-}, "Must throw if it encounters an unknown currencySystem");
-</script>
diff --git a/payment-request/PaymentRequestUpdateEvent/updateWith-incremental-update-manual.https.html b/payment-request/PaymentRequestUpdateEvent/updateWith-incremental-update-manual.https.html
new file mode 100644
index 0000000..6451715
--- /dev/null
+++ b/payment-request/PaymentRequestUpdateEvent/updateWith-incremental-update-manual.https.html
@@ -0,0 +1,187 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#updatewith-method">
+<title>
+  Incremental updates via updateWith()
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({
+  explicit_done: true,
+  explicit_timeout: true,
+});
+
+const methods = [{
+  supportedMethods: "basic-card",
+}];
+
+const options = {
+  requestShipping: true,
+};
+
+const initialDetails = {
+  total: {
+    label: "Initial total",
+    amount: {
+      currency: "USD",
+      value: "1.0",
+    },
+  },
+  shippingOptions: [
+    {
+      id: "neutral",
+      label: "NEUTRAL SHIPPING OPTION",
+      selected: true,
+      amount: {
+        currency: "USD",
+        value: "0.00",
+      },
+    },
+  ],
+};
+
+function testFireEvent(button, updateDetails) {
+  button.disabled = true;
+  const request = new PaymentRequest(methods, initialDetails, options);
+  const handlerPromise = new Promise(resolve => {
+    request.onshippingaddresschange = event => {
+      event.updateWith(updateDetails);
+      resolve(event);
+    };
+  });
+  promise_test(async t => {
+    const response = await request.show();
+    const event = await handlerPromise;
+    await response.complete("success");
+  }, button.textContent.trim());
+}
+
+</script>
+<h2>
+  Incremental updates
+</h2>
+<p>
+  Click on each button in sequence from top to bottom without refreshing the page.
+  Each button will bring up the Payment Request UI window.
+</p>
+<p>
+  Unless stated otherwise, each test will update some part of the displayed payment sheet in
+  a manner indicated below. When prompted, please change or enter a new
+  shipping address, look for the tested change, and complete the payment.
+</p>
+<p>
+  If the payment request locks up or otherwise aborts, the test has failed.
+</p>
+<ol>
+  <li>
+    <button onclick="testFireEvent(this, {});">
+      Passing an empty dictionary does not cause the sheet to change.
+      No values in the sheet must change.
+    </button>
+  </li>
+</ol>
+
+<section>
+  <h3>Incremental updates via PaymentDetailsUpdate.total</h3>
+  <ol>
+    <li>
+      <button onclick="
+      const total = {
+        label: 'PASS',
+        amount: {
+          currency: 'XXX',
+          value: '20',
+        },
+      };
+      const updatedDetails = { total };
+      testFireEvent(this, updatedDetails);">
+        After changing shipping address, the total becomes XXX20, with the label "PASS".
+      </button>
+    </li>
+  </ol>
+</section>
+
+<section>
+  <h3>Incremental updates via PaymentDetailsBase.displayItems</h3>
+  <ol>
+    <li>
+      <button onclick="
+      const item = {
+        label: 'PASS',
+        amount: { currency: 'ABC', value: '55.00' },
+      };
+      const updatedDetails = {
+        displayItems: [ item ]
+      };
+      testFireEvent(this, updatedDetails);">
+        After changing shipping address, a new display item is shown
+        with a with label PASS, and value of ABC55.00.
+      </button>
+    </li>
+  </ol>
+</section>
+
+<section>
+  <h3>Incremental updates via PaymentDetailsBase.shippingOptions</h3>
+  <ol>
+    <li>
+      <button onclick="
+      const shippingOptions = [
+        {
+          id: 'pass',
+          label: 'PASS',
+          amount: { currency: 'USD', value: '1.00' },
+          selected: true,
+        },
+        {
+          id: 'fail',
+          label: 'FAIL IF THIS IS SELECTED',
+          amount: { currency: 'USD', value: '25.00' }
+        },
+      ];
+      const updatedDetails = {
+        shippingOptions
+      };
+      testFireEvent(this, updatedDetails);">
+        After changing shipping address, two new shipping options appear.
+        The shipping option labelled "PASS" with a value of USD1.0 is selected.
+      </button>
+    </li>
+  </ol>
+</section>
+
+<section>
+  <h3>Incremental updates via PaymentDetailsBase.modifiers</h3>
+  <ol>
+    <li>
+      <button onclick="
+      const additionalItem = {
+        label: 'PASS-DISPLAY-ITEM',
+        amount: { currency: 'USD', value: '3.00' },
+      };
+      const modifiers = [{
+        additionalDisplayItems: [ additionalItem ],
+        supportedMethods: 'basic-card',
+        total: {
+          label: 'PASS-TOTAL',
+          amount: { currency: 'USD', value: '123.00' },
+        },
+      }];
+      const updatedDetails = { modifiers };
+      testFireEvent(this, updatedDetails);">
+        After changing shipping address, a new display item is shown
+        with a with label PASS-DISPLAY-ITEM, and value of ABC55.00 and the total is
+        labelled PASS-TOTAL with a value of USD123.0
+      </button>
+    </li>
+    <li>
+      <button onclick="done()">DONE - see results</button>
+    </li>
+  </ol>
+</section>
+
+<small>
+  If you find a buggy test, please <a href="https://github.com/w3c/web-platform-tests/issues">file a bug</a>
+  and tag one of the <a href="https://github.com/w3c/web-platform-tests/blob/master/payment-request/OWNERS">owners</a>.
+</small>
diff --git a/payment-request/change-shipping-option-select-last-manual.https.html b/payment-request/change-shipping-option-select-last-manual.https.html
index c8e23aa..eb1957f 100644
--- a/payment-request/change-shipping-option-select-last-manual.https.html
+++ b/payment-request/change-shipping-option-select-last-manual.https.html
@@ -8,7 +8,7 @@
 setup({ explicit_done: true, explicit_timeout: true });
 const validMethods = Object.freeze([
   { supportedMethods: "basic-card" },
-  { supportedMethods: "https://apple.com/pay" },
+  { supportedMethods: "https://apple.com/apple-pay" },
 ]);
 const validAmount = Object.freeze({ currency: "USD", value: "5.00" });
 const validTotal = Object.freeze({
diff --git a/payment-request/payment-request-abort-method-manual.https.html b/payment-request/payment-request-abort-method-manual.https.html
deleted file mode 100644
index 9552c3c..0000000
--- a/payment-request/payment-request-abort-method-manual.https.html
+++ /dev/null
@@ -1,95 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Test for PaymentRequest.abort() method</title>
-<link rel="help" href="https://w3c.github.io/browser-payment-api/#abort-method">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-"use strict";
-setup({
-  // Ignore unhandled rejections resulting from .show()'s acceptPromise
-  // not being explicitly handled.
-  allow_uncaught_exception: true,
-  explicit_done: true,
-  explicit_timeout: true,
-});
-const basicCard = Object.freeze({ supportedMethods: "basic-card" });
-const defaultMethods = Object.freeze([basicCard]);
-const defaultDetails = Object.freeze({
-  total: {
-    label: "Total",
-    amount: {
-      currency: "USD",
-      value: "1.00",
-    },
-  },
-});
-
-promise_test(async t => {
-  // request is in "created" state
-  const request = new PaymentRequest(defaultMethods, defaultDetails);
-  await promise_rejects(t, "InvalidStateError", request.abort());
-}, `Throws if the promise [[state]] is not "interactive"`);
-
-function manualTest1(elem){
-  elem.disabled = true;
-  promise_test(async t => {
-    // request is in "created" state.
-    const request = new PaymentRequest(defaultMethods, defaultDetails);
-    await promise_rejects(t, "InvalidStateError", request.abort());
-    // Call it again, for good measure.
-    await promise_rejects(t, "InvalidStateError", request.abort());
-    // The request's state is "created", so let's show it
-    // which changes the state to "interactive.".
-    const acceptPromise = request.show();
-    // Let's set request the state to "closed" by calling .abort()
-    try {
-      await request.abort();
-    } catch (err) {
-      assert_unreached("Unexpected promise rejection: " + err.message);
-    }
-    // The request is now "closed", so...
-    await promise_rejects(t, "InvalidStateError", request.abort());
-    await promise_rejects(t, "AbortError", acceptPromise);
-  }, elem.textContent.trim());
-}
-
-function manualTest2(elem){
-  elem.disabled = true;
-  promise_test(async t => {
-    const request = new PaymentRequest(defaultMethods, defaultDetails);
-    const acceptPromise = request.show();
-    try {
-      await request.abort();
-    } catch (err) {
-      assert_unreached("Unexpected promise rejection: " + err.message);
-    }
-    await promise_rejects(t, "AbortError", acceptPromise);
-    // As request is now "closed", trying to show it will fail
-    await promise_rejects(t, "InvalidStateError", request.show());
-  }, elem.textContent.trim());
-  done();
-}
-</script>
-
-<h2>Test for PaymentRequest.abort() method</h2>
-<p>
-  Click on each button in sequence from top to bottom without refreshing the page.
-  No payment sheet will be shown, but the tests will run in the background.
-</p>
-<ol>
-  <li>
-    <button onclick="manualTest1(this)">
-      Calling abort must not change the [[state]] until after "interactive".
-    </button>
-  </li>
-  <li>
-    <button onclick="manualTest2(this)">
-      Calling .abort() causes acceptPromise to reject and closes the request.
-    </button>
-  </li>
-</ol>
-<small>
-  If you find a buggy test, please <a href="https://github.com/w3c/web-platform-tests/issues">file a bug</a>
-  and tag one of the <a href="https://github.com/w3c/web-platform-tests/blob/master/payment-request/OWNERS">owners</a>.
-</small>
diff --git a/payment-request/payment-request-abort-method.https.html b/payment-request/payment-request-abort-method.https.html
new file mode 100644
index 0000000..d1f693a
--- /dev/null
+++ b/payment-request/payment-request-abort-method.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for PaymentRequest.abort() method</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#abort-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script src="/resources/testdriver.js"></script>
+<script>
+"use strict";
+setup({
+  // Ignore unhandled rejections resulting from .show()'s acceptPromise
+  // not being explicitly handled.
+  allow_uncaught_exception: true,
+  explicit_timeout: true,
+});
+const basicCard = Object.freeze({ supportedMethods: "basic-card" });
+const defaultMethods = Object.freeze([basicCard]);
+const defaultDetails = Object.freeze({
+  total: {
+    label: "Total",
+    amount: {
+      currency: "USD",
+      value: "1.00",
+    },
+  },
+});
+
+window.onload = async () => {
+  promise_test(async t => {
+    // request is in "created" state
+    const request = new PaymentRequest(defaultMethods, defaultDetails);
+    await promise_rejects(t, "InvalidStateError", request.abort());
+  }, `Throws if the promise [[state]] is not "interactive"`);
+
+  const button = document.getElementById("button");
+
+  promise_test(async t => {
+    button.onclick = async () => {
+      const request = new PaymentRequest(defaultMethods, defaultDetails);
+      const acceptPromise = request.show();
+      try {
+        await request.abort();
+      } catch (err) {
+        assert_unreached("Unexpected promise rejection: " + err.message);
+      }
+      await promise_rejects(t, "AbortError", acceptPromise);
+      // As request is now "closed", trying to show it will fail
+      await promise_rejects(t, "InvalidStateError", request.show());
+    };
+    await test_driver.click(button);
+  });
+
+  promise_test(async t => {
+    button.onclick = async () => {
+      // request is in "created" state.
+      const request = new PaymentRequest(defaultMethods, defaultDetails);
+      await promise_rejects(t, "InvalidStateError", request.abort());
+      // Call it again, for good measure.
+      await promise_rejects(t, "InvalidStateError", request.abort());
+      // The request's state is "created", so let's show it
+      // which changes the state to "interactive.".
+      const acceptPromise = request.show();
+      // Let's set request the state to "closed" by calling .abort()
+      try {
+        await request.abort();
+      } catch (err) {
+        assert_unreached("Unexpected promise rejection: " + err.message);
+      }
+      // The request is now "closed", so...
+      await promise_rejects(t, "InvalidStateError", request.abort());
+      await promise_rejects(t, "AbortError", acceptPromise);
+    };
+    await test_driver.click(button);
+  });
+};
+</script>
+<button id="button"></button>
diff --git a/payment-request/payment-request-show-method.https.html b/payment-request/payment-request-show-method.https.html
index 32b5668..147e55d 100644
--- a/payment-request/payment-request-show-method.https.html
+++ b/payment-request/payment-request-show-method.https.html
@@ -9,7 +9,7 @@
 'use strict';
 const defaultMethods = Object.freeze([
   { supportedMethods: "basic-card" },
-  { supportedMethods: "https://apple.com/pay" }
+  { supportedMethods: "https://apple.com/apple-pay" }
 ]);
 
 const defaultDetails = Object.freeze({
diff --git a/payment-request/show-method-optional-promise-rejects-manual.https.html b/payment-request/show-method-optional-promise-rejects-manual.https.html
index ab89d7e..1a3b5fe 100644
--- a/payment-request/show-method-optional-promise-rejects-manual.https.html
+++ b/payment-request/show-method-optional-promise-rejects-manual.https.html
@@ -23,7 +23,7 @@
   });
 
   const validMethodApplePay = Object.freeze({
-    supportedMethods: "https://apple.com/pay",
+    supportedMethods: "https://apple.com/apple-pay",
   });
 
   // Methods
diff --git a/payment-request/show-method-optional-promise-resolves-manual.https.html b/payment-request/show-method-optional-promise-resolves-manual.https.html
index 623db15..6ec74bb 100644
--- a/payment-request/show-method-optional-promise-resolves-manual.https.html
+++ b/payment-request/show-method-optional-promise-resolves-manual.https.html
@@ -22,7 +22,7 @@
     supportedMethods: "basic-card",
   },
   {
-    supportedMethods: "https://apple.com/pay",
+    supportedMethods: "https://apple.com/apple-pay",
   },
 ]);
 
diff --git a/payment-request/show-method-postmessage-iframe.html b/payment-request/show-method-postmessage-iframe.html
index fd73d6b..12a1e0c 100644
--- a/payment-request/show-method-postmessage-iframe.html
+++ b/payment-request/show-method-postmessage-iframe.html
@@ -3,7 +3,7 @@
 "use strict";
 const defaultMethods = Object.freeze([
   { supportedMethods: "basic-card" },
-  { supportedMethods: "https://apple.com/pay" },
+  { supportedMethods: "https://apple.com/apple-pay" },
 ]);
 
 const defaultDetails = Object.freeze({
diff --git a/payment-request/show-method-postmessage-manual.https.html b/payment-request/show-method-postmessage-manual.https.html
index 1067fb3..0a7f37f 100644
--- a/payment-request/show-method-postmessage-manual.https.html
+++ b/payment-request/show-method-postmessage-manual.https.html
@@ -14,7 +14,7 @@
 
 const defaultMethods = Object.freeze([
   { supportedMethods: "basic-card" },
-  { supportedMethods: "https://apple.com/pay" },
+  { supportedMethods: "https://apple.com/apple-pay" },
 ]);
 
 const defaultDetails = Object.freeze({
diff --git a/performance-timeline/webtiming-resolution.html b/performance-timeline/webtiming-resolution.html
new file mode 100644
index 0000000..1723c29
--- /dev/null
+++ b/performance-timeline/webtiming-resolution.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+function testTimeResolution(highResTimeFunc, funcString) {
+    test(() => {
+        const t0 = highResTimeFunc();
+        let t1 = highResTimeFunc();
+        while (t0 == t1) {
+            t1 = highResTimeFunc();
+        }
+        assert_greater_than_equal(t1 - t0, 0.02, 'The second ' + funcString + ' should be much greater than the first');
+    }, 'Verifies the resolution of ' + funcString + ' is at least 20 microseconds.');
+}
+
+function timeByPerformanceNow() {
+    return performance.now();
+}
+
+function timeByUserTiming() {
+    performance.mark('timer');
+    const time = performance.getEntriesByName('timer')[0].startTime;
+    performance.clearMarks('timer');
+    return time;
+}
+
+testTimeResolution(timeByPerformanceNow, 'performance.now()');
+testTimeResolution(timeByUserTiming, 'entry.startTime');
+</script>
+</body>
+</html>
diff --git a/pointerevents/extension/idlharness.html b/pointerevents/extension/idlharness.html
index 7da7fed..e2835f8 100644
--- a/pointerevents/extension/idlharness.html
+++ b/pointerevents/extension/idlharness.html
@@ -52,10 +52,15 @@
   const dom = await fetch('/interfaces/dom.idl').then(r => r.text());
   const uievents = await fetch('/interfaces/uievents.idl').then(r => r.text());
 
-  var idl_array = new IdlArray();
+  const idl_array = new IdlArray();
   idl_array.add_untested_idls(dom, { only: ['EventInit'] });
-  idl_array.add_untested_idls(uievents, { only: ['UIEventInit', 'MouseEventInit', 'EventModifierInit'] });
-  idl_array.add_untested_idls(document.getElementById("untested_idl").textContent);
+  idl_array.add_untested_idls(uievents, { only: [
+    'UIEventInit',
+    'MouseEventInit',
+    'EventModifierInit']
+  });
+  idl_array.add_untested_idls(
+      document.getElementById("untested_idl").textContent);
   idl_array.add_idls(document.getElementById("idl").textContent);
   idl_array.test();
 }, 'pointerevents extension interfaces');
diff --git a/pointerevents/idlharness.html b/pointerevents/idlharness.html
index bdc7ad7..6afcb21 100644
--- a/pointerevents/idlharness.html
+++ b/pointerevents/idlharness.html
@@ -95,10 +95,15 @@
     const dom = await fetch('/interfaces/dom.idl').then(r => r.text());
     const uievents = await fetch('/interfaces/uievents.idl').then(r => r.text());
 
-    var idl_array = new IdlArray();
+    const idl_array = new IdlArray();
     idl_array.add_untested_idls(dom, { only: ['EventInit'] });
-    idl_array.add_untested_idls(uievents, { only: ['UIEventInit', 'MouseEventInit', 'EventModifierInit'] });
-    idl_array.add_untested_idls(document.getElementById("untested_idl").textContent);
+    idl_array.add_untested_idls(uievents, { only: [
+      'UIEventInit',
+      'MouseEventInit',
+      'EventModifierInit']
+    });
+    idl_array.add_untested_idls(
+        document.getElementById("untested_idl").textContent);
     idl_array.add_idls(document.getElementById("idl").textContent);
 
     // Note that I don't bother including Document here because there are still
diff --git a/pointerevents/pointerevent_support.js b/pointerevents/pointerevent_support.js
index 64f1348..bf68f85 100644
--- a/pointerevents/pointerevent_support.js
+++ b/pointerevents/pointerevent_support.js
@@ -39,7 +39,8 @@
         "long": function (v) { return typeof v === "number" && Math.round(v) === v; },
         "float": function (v) { return typeof v === "number"; },
         "string": function (v) { return typeof v === "string"; },
-        "boolean": function (v) { return typeof v === "boolean" }
+        "boolean": function (v) { return typeof v === "boolean" },
+        "object": function (v) { return typeof v === "object" }
     };
     [
         ["readonly", "long", "pointerId"],
@@ -50,7 +51,9 @@
         ["readonly", "long", "tiltY"],
         ["readonly", "string", "pointerType"],
         ["readonly", "boolean", "isPrimary"],
-        ["readonly", "long", "detail", 0]
+        ["readonly", "long", "detail", 0],
+        ["readonly", "object", "fromElement", null],
+        ["readonly", "object", "toElement", null]
     ].forEach(function (attr) {
         var readonly = attr[0];
         var type = attr[1];
@@ -75,7 +78,7 @@
         }, pointerTestName + "." + name + " IDL type " + type + " (JS type was " + typeof event[name] + ")");
 
         // value check if defined
-        if (value != undefined) {
+        if (value !== undefined) {
             test(function () {
                 assert_equals(event[name], value, name + " attribute value");
             }, pointerTestName + "." + name + " value is " + value + ".");
diff --git a/referrer-policy/generic/common.js b/referrer-policy/generic/common.js
index 3459401..1ccdaab 100644
--- a/referrer-policy/generic/common.js
+++ b/referrer-policy/generic/common.js
@@ -248,7 +248,3 @@
 function SanityChecker() {}
 SanityChecker.prototype.checkScenario = function() {};
 SanityChecker.prototype.checkSubresourceResult = function() {};
-
-// TODO(kristijanburnik): document.origin is supported since Chrome 41,
-// other browsers still don't support it. Remove once they do.
-document.origin = document.origin || (location.protocol + "//" + location.host);
diff --git a/referrer-policy/generic/referrer-policy-test-case.js b/referrer-policy/generic/referrer-policy-test-case.js
index b90ecfd..7c8cba4 100644
--- a/referrer-policy/generic/referrer-policy-test-case.js
+++ b/referrer-policy/generic/referrer-policy-test-case.js
@@ -29,7 +29,7 @@
       return undefined;
     },
     "origin": function() {
-      return document.origin + "/";
+      return self.origin + "/";
     },
     "stripped-referrer": function() {
       return stripUrlForUseAsReferrer(location.toString());
diff --git a/resize-observer/eventloop.html b/resize-observer/eventloop.html
index 4ef1d3d..559f52a 100644
--- a/resize-observer/eventloop.html
+++ b/resize-observer/eventloop.html
@@ -129,7 +129,7 @@
         let t3 = document.createElement('div');
         resizers.push(t3);
         t2.appendChild(t3);
-        let shadow = t3.createShadowRoot();
+        let shadow = t3.attachShadow({ mode: "open" });
         let t4 = document.createElement('div');
         resizers.push(t4);
         shadow.appendChild(t4);
diff --git a/resource-timing/resources/TAOResponse.py b/resource-timing/resources/TAOResponse.py
index cc8fa5f..dcd75a4 100644
--- a/resource-timing/resources/TAOResponse.py
+++ b/resource-timing/resources/TAOResponse.py
@@ -20,19 +20,19 @@
     # space seperated list of origin and wildcard, fail
         response.headers.set('Timing-Allow-Origin', (origin + ' *'))
     elif tao == 'multi':
-    # more than one TAO values, seperated by common, pass
+    # more than one TAO values, seperated by comma, pass
         response.headers.set('Timing-Allow-Origin', origin)
         response.headers.append('Timing-Allow-Origin', '*')
     elif tao == 'match_origin':
-    # contains a match of origin, seperated by common, pass
+    # contains a match of origin, seperated by comma, pass
         response.headers.set('Timing-Allow-Origin', origin)
         response.headers.append('Timing-Allow-Origin', "fake")
     elif tao == 'match_wildcard':
-    # contains a wildcard, seperated by common, pass
+    # contains a wildcard, seperated by comma, pass
         response.headers.set('Timing-Allow-Origin', "fake")
         response.headers.append('Timing-Allow-Origin', '*')
     elif tao == 'uppercase':
     # non-case-sensitive match for origin, fail
         response.headers.set('Timing-Allow-Origin', origin.upper())
     else:
-        pass
\ No newline at end of file
+        pass
diff --git a/resources/idlharness.js b/resources/idlharness.js
index c5c0c98..4664465 100644
--- a/resources/idlharness.js
+++ b/resources/idlharness.js
@@ -1513,8 +1513,6 @@
         // object.
         // "Otherwise, if A is declared to inherit from another interface, then
         // return the interface prototype object for the inherited interface.
-        // "Otherwise, if A is declared with the [LegacyArrayClass] extended
-        // attribute, then return %ArrayPrototype%.
         // "Otherwise, return %ObjectPrototype%.
         //
         // "In the ECMAScript binding, the DOMException type has some additional
@@ -1537,9 +1535,6 @@
                     !this.array
                          .members[inherit_interface]
                          .has_extended_attribute("NoInterfaceObject");
-            } else if (this.has_extended_attribute('LegacyArrayClass')) {
-                inherit_interface = 'Array';
-                inherit_interface_has_interface_object = true;
             } else if (this.name === "DOMException") {
                 inherit_interface = 'Error';
                 inherit_interface_has_interface_object = true;
@@ -2166,7 +2161,7 @@
 //@{
 {
     var interfaceName = this.name;
-    var isPairIterator = member.idlType instanceof Array;
+    var isPairIterator = member.idlType.length === 2;
     test(function()
     {
         var descriptor = Object.getOwnPropertyDescriptor(self[interfaceName].prototype, Symbol.iterator);
diff --git a/resources/test/conftest.py b/resources/test/conftest.py
index 0be41ee..abc3b6c 100644
--- a/resources/test/conftest.py
+++ b/resources/test/conftest.py
@@ -80,7 +80,7 @@
         if not self.expected:
             assert summarized[u'summarized_status'][u'status_string'] == u'OK', summarized[u'summarized_status'][u'message']
             for test in summarized[u'summarized_tests']:
-                msg = "%s\n%s:\n%s" % (test[u'name'], test[u'message'], test[u'stack'])
+                msg = "%s\n%s" % (test[u'name'], test[u'message'])
                 assert test[u'status_string'] == u'PASS', msg
         else:
             assert summarized == self.expected
@@ -93,12 +93,7 @@
     @staticmethod
     def _scrub_stack(test_obj):
         copy = dict(test_obj)
-
-        assert 'stack' in copy
-
-        if copy['stack'] is not None:
-            copy['stack'] = u'(implementation-defined)'
-
+        del copy['stack']
         return copy
 
     @staticmethod
diff --git a/resources/test/tests/add_cleanup.html b/resources/test/tests/add_cleanup.html
index d47e7ff..c5e0ad7 100644
--- a/resources/test/tests/add_cleanup.html
+++ b/resources/test/tests/add_cleanup.html
@@ -56,37 +56,32 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Cleanup methods are invoked exactly once and in the expected sequence.",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Cleanup methods are invoked following the completion of asynchronous tests",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "probe asynchronous",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "probe synchronous",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/add_cleanup_count.html b/resources/test/tests/add_cleanup_count.html
index a079c9b..3ea30a3 100644
--- a/resources/test/tests/add_cleanup_count.html
+++ b/resources/test/tests/add_cleanup_count.html
@@ -22,14 +22,12 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed.",
-    "stack": null
+    "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed."
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "test with 3 user-defined cleanup functions",
-      "stack": null,
       "message": null,
       "properties": {}
     }
diff --git a/resources/test/tests/add_cleanup_err.html b/resources/test/tests/add_cleanup_err.html
index 66efb07..bae1eb5 100644
--- a/resources/test/tests/add_cleanup_err.html
+++ b/resources/test/tests/add_cleanup_err.html
@@ -22,23 +22,20 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed.",
-    "stack": null
+    "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed."
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Exception in cleanup function causes harness failure.",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "NOTRUN",
       "name": "This test should not be run.",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/add_cleanup_err_multi.html b/resources/test/tests/add_cleanup_err_multi.html
index b60668c..3f116c5 100644
--- a/resources/test/tests/add_cleanup_err_multi.html
+++ b/resources/test/tests/add_cleanup_err_multi.html
@@ -29,23 +29,20 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed.",
-    "stack": null
+    "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed."
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Test with multiple cleanup functions",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "NOTRUN",
       "name": "Verification test",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/api-tests-1.html b/resources/test/tests/api-tests-1.html
index 3c58b01..3b5234f 100644
--- a/resources/test/tests/api-tests-1.html
+++ b/resources/test/tests/api-tests-1.html
@@ -208,161 +208,138 @@
 {
   "summarized_status": {
     "status_string": "TIMEOUT",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Setup function ran",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "Test assert_throws with non-DOM-exception expected to Fail",
-      "stack": "(implementation-defined)",
       "message": "Test bug: unrecognized DOMException code \"TEST_ERR\" passed to assert_throws()",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test async test with callback",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test async test with callback and `this` obj.",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test step_func",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test that cleanup handlers from previous test ran",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test that defines a global and cleans it up",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Test throw DOM exception",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "Test throw DOM exception expected to fail",
-      "stack": "(implementation-defined)",
       "message": "assert_throws: function \"function () {a.appendChild(b)}\" threw object \"HierarchyRequestError: Node cannot be inserted at the specified point in the hierarchy\" that is not a DOMException NOT_FOUND_ERR: property \"code\" is equal to 3, expected 8",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_array_equals with first param 1",
-      "stack": "(implementation-defined)",
       "message": "assert_array_equals: 1 equals [1]? value is 1, expected array",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_array_equals with first param false",
-      "stack": "(implementation-defined)",
       "message": "assert_array_equals: false equals [1]? value is false, expected array",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_array_equals with first param null",
-      "stack": "(implementation-defined)",
       "message": "assert_array_equals: null equals [1]? value is null, expected array",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_array_equals with first param true",
-      "stack": "(implementation-defined)",
       "message": "assert_array_equals: true equals [1]? value is true, expected array",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_array_equals with first param undefined",
-      "stack": "(implementation-defined)",
       "message": "assert_array_equals: undefined equals [1]? value is undefined, expected array",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "assert_equals tests",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_equals tests expected to fail",
-      "stack": "(implementation-defined)",
       "message": "assert_equals: Zero case expected 0 but got -0",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_greater_than expected to fail",
-      "stack": "(implementation-defined)",
       "message": "assert_greater_than: 10 is not greater than 11 expected a number greater than 11 but got 10",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_less_than_equal expected to fail",
-      "stack": "(implementation-defined)",
       "message": "assert_greater_than_equal: '10' is not a number expected a number but got a \"string\"",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "assert_not_equals tests",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "assert_true expected to fail",
-      "stack": "(implementation-defined)",
       "message": "assert_true: false should not be true expected true got false",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "assert_true expected to pass",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "NOTRUN",
       "name": "async test that is never started, should have status Not Run",
-      "stack": null,
       "message": null,
       "properties": {
         "timeout": 1000
@@ -371,70 +348,60 @@
     {
       "status_string": "PASS",
       "name": "basic assert_approx_equals test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "basic assert_array_approx_equals test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "basic assert_array_equals test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "basic assert_greater_than_equal test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "basic assert_less_than test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "basic assert_object_equals test",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "body element fires the onload event set from the attribute",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "document.body should be the first body element in the document",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "test for assert[_not]_exists and insert_inherits",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "TIMEOUT",
       "name": "test should timeout (fail) with a custom set timeout value of 1 second",
-      "stack": null,
       "message": "Test timed out",
       "properties": {
         "timeout": 1000
@@ -443,14 +410,12 @@
     {
       "status_string": "TIMEOUT",
       "name": "test should timeout (fail) with the default of 2 seconds",
-      "stack": null,
       "message": "Test timed out",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "window onload event fires when set from the handler",
-      "stack": null,
       "message": null,
       "properties": {}
     }
diff --git a/resources/test/tests/api-tests-2.html b/resources/test/tests/api-tests-2.html
index dd08d1a..164106b 100644
--- a/resources/test/tests/api-tests-2.html
+++ b/resources/test/tests/api-tests-2.html
@@ -21,23 +21,20 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Test defined after onload",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test defined before onload",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/api-tests-3.html b/resources/test/tests/api-tests-3.html
index 9e87e49..5dbeb2d 100644
--- a/resources/test/tests/api-tests-3.html
+++ b/resources/test/tests/api-tests-3.html
@@ -17,16 +17,14 @@
 {
   "summarized_status": {
     "status_string": "TIMEOUT",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "NOTRUN",
       "name": "This test should give a status of 'Not Run' without a delay",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/force_timeout.html b/resources/test/tests/force_timeout.html
index bef53b4..7827068 100644
--- a/resources/test/tests/force_timeout.html
+++ b/resources/test/tests/force_timeout.html
@@ -31,29 +31,25 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "TIMEOUT",
       "name": "async_test",
       "message": "Test timed out",
-      "stack": null,
       "properties": {}
     },
     {
       "status_string": "TIMEOUT",
       "name": "promise_test",
       "message": "Test timed out",
-      "stack": null,
       "properties": {}
     },
     {
       "status_string": "TIMEOUT",
       "name": "test (synchronous)",
       "message": "Test timed out",
-      "stack": null,
       "properties": {}
     }
   ],
diff --git a/resources/test/tests/generate-callback.html b/resources/test/tests/generate-callback.html
index fe46989..574f430 100644
--- a/resources/test/tests/generate-callback.html
+++ b/resources/test/tests/generate-callback.html
@@ -56,65 +56,56 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Test to map a to 0",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test to map b to 1",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test to map c to 2",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test to map d to 3",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test to map e to 4",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test to map f to 5",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "first test",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "second test",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
@@ -122,8 +113,7 @@
       "properties": {
         "sentinel": true
       },
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
@@ -131,8 +121,7 @@
       "properties": {
         "sentinel": true
       },
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
@@ -140,8 +129,7 @@
       "properties": {
         "sentinel": true
       },
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
@@ -149,15 +137,13 @@
       "properties": {
         "sentinel": false
       },
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "third test",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/idlharness/IdlInterface/test_immutable_prototype.html b/resources/test/tests/idlharness/IdlInterface/test_immutable_prototype.html
index 480a159..a9d631d 100644
--- a/resources/test/tests/idlharness/IdlInterface/test_immutable_prototype.html
+++ b/resources/test/tests/idlharness/IdlInterface/test_immutable_prototype.html
@@ -43,289 +43,248 @@
 {
     "summarized_status": {
         "message": null,
-        "status_string": "OK",
-        "stack": null
+        "status_string": "OK"
     },
     "summarized_tests": [
         {
             "name": "Foo interface object length",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface object name",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: existence and properties of interface object",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: existence and properties of interface prototype object",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_throws: function \"function() {\n            Object.setPrototypeOf(obj, newValue);\n        }\" did not throw",
-            "stack": "(implementation-defined)"
+            "message": "assert_throws: function \"function() {\n            Object.setPrototypeOf(obj, newValue);\n        }\" did not throw"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_false: expected false got true",
-            "stack": "(implementation-defined)"
+            "message": "assert_false: expected false got true"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_throws: function \"function() {\n            obj.__proto__ = newValue;\n        }\" did not throw",
-            "stack": "(implementation-defined)"
+            "message": "assert_throws: function \"function() {\n            obj.__proto__ = newValue;\n        }\" did not throw"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_throws: function \"function() {\n            Object.setPrototypeOf(obj, newValue);\n        }\" did not throw",
-            "stack": "(implementation-defined)"
+            "message": "assert_throws: function \"function() {\n            Object.setPrototypeOf(obj, newValue);\n        }\" did not throw"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_false: expected false got true",
-            "stack": "(implementation-defined)"
+            "message": "assert_false: expected false got true"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
             "status_string": "FAIL",
             "properties": {},
-            "message": "assert_throws: function \"function() {\n            obj.__proto__ = newValue;\n        }\" did not throw",
-            "stack": "(implementation-defined)"
+            "message": "assert_throws: function \"function() {\n            obj.__proto__ = newValue;\n        }\" did not throw"
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Foo must be primary interface of new Foo()",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Stringification of new Foo()",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Stringification of window",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface object length",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface object name",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: existence and properties of interface object",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: existence and properties of interface prototype object",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: existence and properties of interface prototype object's \"constructor\" property",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: existence and properties of interface prototype object's @@unscopables property",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         },
         {
             "name": "Window must be primary interface of window",
             "status_string": "PASS",
             "properties": {},
-            "message": null,
-            "stack": null
+            "message": null
         }
     ],
     "type": "complete"
diff --git a/resources/test/tests/idlharness/IdlInterface/test_primary_interface_of.html b/resources/test/tests/idlharness/IdlInterface/test_primary_interface_of.html
index 43241df..69542f1 100644
--- a/resources/test/tests/idlharness/IdlInterface/test_primary_interface_of.html
+++ b/resources/test/tests/idlharness/IdlInterface/test_primary_interface_of.html
@@ -50,65 +50,56 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "name": "Foo interface object length",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo interface object name",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo interface: existence and properties of interface object",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo interface: existence and properties of interface prototype object",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Foo must be primary interface of new Foo()",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "name": "Stringification of new Foo()",
       "status_string": "PASS",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/idlharness/IdlInterface/test_to_json_operation.html b/resources/test/tests/idlharness/IdlInterface/test_to_json_operation.html
index d9c3fcf..ef8fcc9 100644
--- a/resources/test/tests/idlharness/IdlInterface/test_to_json_operation.html
+++ b/resources/test/tests/idlharness/IdlInterface/test_to_json_operation.html
@@ -94,7 +94,6 @@
     {
         "summarized_status": {
             "message": null,
-            "stack": null,
             "status_string": "OK"
         },
         "summarized_tests": [
@@ -102,84 +101,72 @@
                 "message": null,
                 "name": "Test default toJSON operation of A",
                 "properties": {},
-                "stack": null,
                 "status_string": "PASS"
             },
             {
                 "message": "assert_equals: expected \"number\" but got \"string\"",
                 "name": "Test default toJSON operation of B",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
                 "message": null,
                 "name": "Test default toJSON operation of C",
                 "properties": {},
-                "stack": null,
                 "status_string": "PASS"
             },
             {
                 "message": "assert_true: property \"foo\" should be present in the output of D.prototype.toJSON() expected true got false",
                 "name": "Test default toJSON operation of D",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
                 "message": "assert_true: property foo should be writable expected true got false",
                 "name": "Test default toJSON operation of F",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
                 "message": "assert_true: property foo should be enumerable expected true got false",
                 "name": "Test default toJSON operation of G",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
                 "message": "assert_true: property foo should be configurable expected true got false",
                 "name": "Test default toJSON operation of H",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
                 "message": null,
                 "name": "Test default toJSON operation of I",
                 "properties": {},
-                "stack": null,
                 "status_string": "PASS"
             },
             {
                 "message": null,
                 "name": "Test default toJSON operation of K",
                 "properties": {},
-                "stack": null,
                 "status_string": "PASS"
             },
             {
                 "message": null,
                 "name": "Test toJSON operation of L",
                 "properties": {},
-                "stack": null,
                 "status_string": "PASS"
             },
             {
                 "message": "assert_equals: expected \"string\" but got \"object\"",
                 "name": "Test toJSON operation of M",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             },
             {
-                "message": "assert_true: {\"sequence\":false,\"generic\":null,\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false",
+                "message": "assert_true: {\"type\":\"return-type\",\"sequence\":false,\"generic\":null,\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false",
                 "name": "Test toJSON operation of N",
                 "properties": {},
-                "stack": "(implementation-defined)",
                 "status_string": "FAIL"
             }
         ],
diff --git a/resources/test/tests/iframe-callback.html b/resources/test/tests/iframe-callback.html
index 1a8b2cb..b6b751b 100644
--- a/resources/test/tests/iframe-callback.html
+++ b/resources/test/tests/iframe-callback.html
@@ -100,16 +100,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Example with iframe that notifies containing document via callbacks",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/iframe-consolidate-errors.html b/resources/test/tests/iframe-consolidate-errors.html
index 5d697e4..a44d034 100644
--- a/resources/test/tests/iframe-consolidate-errors.html
+++ b/resources/test/tests/iframe-consolidate-errors.html
@@ -27,16 +27,14 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Error in remote: Error: Example Error",
-    "stack": null
+    "message": "Error in remote: Error: Example Error"
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Test executing in parent context",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/iframe-consolidate-tests.html b/resources/test/tests/iframe-consolidate-tests.html
index 5611d57..7b8a897 100644
--- a/resources/test/tests/iframe-consolidate-tests.html
+++ b/resources/test/tests/iframe-consolidate-tests.html
@@ -27,65 +27,56 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Promise rejection",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Promise resolution",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Promises and test assertion failures (should fail)",
       "properties": {},
-      "message": "assert_true: This failure is expected expected true got false",
-      "stack": "(implementation-defined)"
+      "message": "assert_true: This failure is expected expected true got false"
     },
     {
       "status_string": "PASS",
       "name": "Promises are supported in your browser",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Promises resolution chaining",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test executing in parent context",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Use of step_func with Promises",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Use of unreached_func with Promises (should fail)",
       "properties": {},
-      "message": "assert_unreached: This failure is expected Reached unreachable code",
-      "stack": "(implementation-defined)"
+      "message": "assert_unreached: This failure is expected Reached unreachable code"
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/iframe-msg.html b/resources/test/tests/iframe-msg.html
index 13a3fff..c6bc97b 100644
--- a/resources/test/tests/iframe-msg.html
+++ b/resources/test/tests/iframe-msg.html
@@ -68,16 +68,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Containing document receives messages",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/order.html b/resources/test/tests/order.html
index d20eb6b..1604f43 100644
--- a/resources/test/tests/order.html
+++ b/resources/test/tests/order.html
@@ -16,19 +16,16 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [{
     "status_string": "PASS",
     "name": "first",
-    "stack": null,
     "message": null,
     "properties": {}
   }, {
     "status_string": "PASS",
     "name": "second",
-    "stack": null,
     "message": null,
     "properties": {}
   }],
diff --git a/resources/test/tests/promise-async.html b/resources/test/tests/promise-async.html
index 88b6972..18101ee 100644
--- a/resources/test/tests/promise-async.html
+++ b/resources/test/tests/promise-async.html
@@ -121,58 +121,50 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Promise rejection",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Promise resolution",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Promises and test assertion failures (should fail)",
       "properties": {},
-      "message": "assert_true: This failure is expected expected true got false",
-      "stack": "(implementation-defined)"
+      "message": "assert_true: This failure is expected expected true got false"
     },
     {
       "status_string": "PASS",
       "name": "Promises are supported in your browser",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Promises resolution chaining",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Use of step_func with Promises",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Use of unreached_func with Promises (should fail)",
       "properties": {},
-      "message": "assert_unreached: This failure is expected Reached unreachable code",
-      "stack": "(implementation-defined)"
+      "message": "assert_unreached: This failure is expected Reached unreachable code"
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/promise.html b/resources/test/tests/promise.html
index 61ef15d..4825a38 100644
--- a/resources/test/tests/promise.html
+++ b/resources/test/tests/promise.html
@@ -134,84 +134,72 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "FAIL",
       "name": "Assertion failure in a fulfill reaction (should FAIL with an expected failure)",
-      "stack": "(implementation-defined)",
       "message": "assert_true: Expected failure. expected true got false",
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Chain of promise resolutions",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Promise fulfillment with result",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Promise rejection with result",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "Promises are supported in your browser",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "PASS",
       "name": "promise_test with function that doesn't return a Promise",
-      "stack": null,
       "message": null,
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "promise_test with function that doesn't return anything",
-      "stack": "(implementation-defined)",
       "message": "assert_not_equals: got disallowed value undefined",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "promise_test with unhandled exception in fulfill reaction (should FAIL)",
-      "stack": "(implementation-defined)",
       "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "promise_test with unhandled exception in reject reaction (should FAIL)",
-      "stack": "(implementation-defined)",
       "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "promise_test with unhandled rejection (should FAIL)",
-      "stack": "(implementation-defined)",
       "message": "promise_test: Unhandled rejection with value: \"Expected rejection\"",
       "properties": {}
     },
     {
       "status_string": "FAIL",
       "name": "unreached_func as reactor (should FAIL with an expected failure)",
-      "stack": "(implementation-defined)",
       "message": "assert_unreached: Expected failure. Reached unreachable code",
       "properties": {}
     }
diff --git a/resources/test/tests/single-page-test-fail.html b/resources/test/tests/single-page-test-fail.html
index a1f8810..a8f247d 100644
--- a/resources/test/tests/single-page-test-fail.html
+++ b/resources/test/tests/single-page-test-fail.html
@@ -12,16 +12,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "FAIL",
       "name": "Example with file_is_test (should fail)",
       "properties": {},
-      "message": "uncaught exception: Error: assert_true: expected true got false",
-      "stack": "(implementation-defined)"
+      "message": "uncaught exception: Error: assert_true: expected true got false"
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/single-page-test-no-assertions.html b/resources/test/tests/single-page-test-no-assertions.html
index 3402a46..d1a7532 100644
--- a/resources/test/tests/single-page-test-no-assertions.html
+++ b/resources/test/tests/single-page-test-no-assertions.html
@@ -9,16 +9,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Example single page test with no asserts",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/single-page-test-no-body.html b/resources/test/tests/single-page-test-no-body.html
index 065091a..c62ee18 100644
--- a/resources/test/tests/single-page-test-no-body.html
+++ b/resources/test/tests/single-page-test-no-body.html
@@ -10,16 +10,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Example single page test with no body",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/single-page-test-pass.html b/resources/test/tests/single-page-test-pass.html
index 31e4e64..91d26b0 100644
--- a/resources/test/tests/single-page-test-pass.html
+++ b/resources/test/tests/single-page-test-pass.html
@@ -12,16 +12,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Example with file_is_test",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/uncaught-exception-handle.html b/resources/test/tests/uncaught-exception-handle.html
index 7d8244b..061fb80 100644
--- a/resources/test/tests/uncaught-exception-handle.html
+++ b/resources/test/tests/uncaught-exception-handle.html
@@ -16,16 +16,14 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Error: Example Error",
-    "stack": "(implementation-defined)"
+    "message": "Error: Example Error"
   },
   "summarized_tests": [
     {
       "status_string": "NOTRUN",
       "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/uncaught-exception-ignore.html b/resources/test/tests/uncaught-exception-ignore.html
index 9f45ddb..6624078 100644
--- a/resources/test/tests/uncaught-exception-ignore.html
+++ b/resources/test/tests/uncaught-exception-ignore.html
@@ -18,16 +18,14 @@
 {
   "summarized_status": {
     "status_string": "OK",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/worker-dedicated.html b/resources/test/tests/worker-dedicated.html
index 0ca3e4d..98a010d 100644
--- a/resources/test/tests/worker-dedicated.html
+++ b/resources/test/tests/worker-dedicated.html
@@ -30,65 +30,56 @@
 {
   "summarized_status": {
     "status_string": "ERROR",
-    "message": "Error: This failure is expected.",
-    "stack": "(implementation-defined)"
+    "message": "Error: This failure is expected."
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Browser supports Workers",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Test running on main document.",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Untitled",
       "properties": {},
-      "message": "Error: This failure is expected.",
-      "stack": "(implementation-defined)"
+      "message": "Error: This failure is expected."
     },
     {
       "status_string": "PASS",
       "name": "Worker async_test that completes successfully",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Worker test that completes successfully",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "NOTRUN",
       "name": "Worker test that doesn't run ('NOT RUN')",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Worker test that fails ('FAIL')",
       "properties": {},
-      "message": "assert_true: Failing test expected true got false",
-      "stack": "(implementation-defined)"
+      "message": "assert_true: Failing test expected true got false"
     },
     {
       "status_string": "TIMEOUT",
       "name": "Worker test that times out ('TIMEOUT')",
       "properties": {},
-      "message": "Test timed out",
-      "stack": null
+      "message": "Test timed out"
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/worker-service.html b/resources/test/tests/worker-service.html
index 418f6d7..d970827 100644
--- a/resources/test/tests/worker-service.html
+++ b/resources/test/tests/worker-service.html
@@ -63,58 +63,50 @@
 {
   "summarized_status": {
     "status_string": "TIMEOUT",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Browser supports ServiceWorker",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Register ServiceWorker",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Worker async_test that completes successfully",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "PASS",
       "name": "Worker test that completes successfully",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "NOTRUN",
       "name": "Worker test that doesn't run ('NOT RUN')",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "status_string": "FAIL",
       "name": "Worker test that fails ('FAIL')",
       "properties": {},
-      "message": "assert_true: Failing test expected true got false",
-      "stack": "(implementation-defined)"
+      "message": "assert_true: Failing test expected true got false"
     },
     {
       "status_string": "TIMEOUT",
       "name": "Worker test that times out ('TIMEOUT')",
       "properties": {},
-      "message": "Test timed out",
-      "stack": null
+      "message": "Test timed out"
     }
   ],
   "type": "complete"
diff --git a/resources/test/tests/worker-shared.html b/resources/test/tests/worker-shared.html
index a5601de..9c5666b 100644
--- a/resources/test/tests/worker-shared.html
+++ b/resources/test/tests/worker-shared.html
@@ -27,50 +27,43 @@
 {
   "summarized_status": {
     "status_string": "TIMEOUT",
-    "message": null,
-    "stack": null
+    "message": null
   },
   "summarized_tests": [
     {
       "status_string": "PASS",
       "name": "Browser supports SharedWorkers",
       "properties": {},
-      "message": null,
-      "stack": null
+      "message": null
     },
     {
       "message": null,
       "name": "Worker async_test that completes successfully",
       "properties": {},
-      "stack": null,
       "status_string": "PASS"
     },
     {
       "message": null,
       "name": "Worker test that completes successfully",
       "properties": {},
-      "stack": null,
       "status_string": "PASS"
     },
     {
       "message": null,
       "name": "Worker test that doesn't run ('NOT RUN')",
       "properties": {},
-      "stack": null,
       "status_string": "NOTRUN"
     },
     {
       "message": "assert_true: Failing test expected true got false",
       "name": "Worker test that fails ('FAIL')",
       "properties": {},
-      "stack": "(implementation-defined)",
       "status_string": "FAIL"
     },
     {
       "message": "Test timed out",
       "name": "Worker test that times out ('TIMEOUT')",
       "properties": {},
-      "stack": null,
       "status_string": "TIMEOUT"
     }
   ],
diff --git a/resources/testharness.js b/resources/testharness.js
index ec0090c..39a467c 100644
--- a/resources/testharness.js
+++ b/resources/testharness.js
@@ -13,7 +13,7 @@
 /* Documentation: http://web-platform-tests.org/writing-tests/testharness-api.html
  * (../docs/_writing-tests/testharness-api.md) */
 
-(function ()
+(function (global_scope)
 {
     var debug = false;
     // default timeout is 10 seconds, test can override if needed
@@ -48,9 +48,6 @@
      *
      *   // Should return the test harness timeout duration in milliseconds.
      *   float test_timeout();
-     *
-     *   // Should return the global scope object.
-     *   object global_scope();
      * };
      */
 
@@ -248,10 +245,6 @@
         return settings.harness_timeout.normal;
     };
 
-    WindowTestEnvironment.prototype.global_scope = function() {
-        return window;
-    };
-
     /*
      * Base TestEnvironment implementation for a generic web worker.
      *
@@ -344,10 +337,6 @@
         return null;
     };
 
-    WorkerTestEnvironment.prototype.global_scope = function() {
-        return self;
-    };
-
     /*
      * Dedicated web workers.
      * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
@@ -463,23 +452,23 @@
     };
 
     function create_test_environment() {
-        if ('document' in self) {
+        if ('document' in global_scope) {
             return new WindowTestEnvironment();
         }
-        if ('DedicatedWorkerGlobalScope' in self &&
-            self instanceof DedicatedWorkerGlobalScope) {
+        if ('DedicatedWorkerGlobalScope' in global_scope &&
+            global_scope instanceof DedicatedWorkerGlobalScope) {
             return new DedicatedWorkerTestEnvironment();
         }
-        if ('SharedWorkerGlobalScope' in self &&
-            self instanceof SharedWorkerGlobalScope) {
+        if ('SharedWorkerGlobalScope' in global_scope &&
+            global_scope instanceof SharedWorkerGlobalScope) {
             return new SharedWorkerTestEnvironment();
         }
-        if ('ServiceWorkerGlobalScope' in self &&
-            self instanceof ServiceWorkerGlobalScope) {
+        if ('ServiceWorkerGlobalScope' in global_scope &&
+            global_scope instanceof ServiceWorkerGlobalScope) {
             return new ServiceWorkerTestEnvironment();
         }
-        if ('WorkerGlobalScope' in self &&
-            self instanceof WorkerGlobalScope) {
+        if ('WorkerGlobalScope' in global_scope &&
+            global_scope instanceof WorkerGlobalScope) {
             return new DedicatedWorkerTestEnvironment();
         }
 
@@ -489,13 +478,13 @@
     var test_environment = create_test_environment();
 
     function is_shared_worker(worker) {
-        return 'SharedWorker' in self && worker instanceof SharedWorker;
+        return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
     }
 
     function is_service_worker(worker) {
         // The worker object may be from another execution context,
         // so do not use instanceof here.
-        return 'ServiceWorker' in self &&
+        return 'ServiceWorker' in global_scope &&
             Object.prototype.toString.call(worker) == '[object ServiceWorker]';
     }
 
@@ -2824,7 +2813,7 @@
     function expose(object, name)
     {
         var components = name.split(".");
-        var target = test_environment.global_scope();
+        var target = global_scope;
         for (var i = 0; i < components.length - 1; i++) {
             if (!(components[i] in target)) {
                 target[components[i]] = {};
@@ -2846,7 +2835,7 @@
     /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
     function get_script_url()
     {
-        if (!('document' in self)) {
+        if (!('document' in global_scope)) {
             return undefined;
         }
 
@@ -2954,5 +2943,5 @@
 
     test_environment.on_tests_ready();
 
-})();
+})(this);
 // vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/resources/webidl2/.travis.yml b/resources/webidl2/.travis.yml
index fc15089..6dc8e2b 100644
--- a/resources/webidl2/.travis.yml
+++ b/resources/webidl2/.travis.yml
@@ -2,3 +2,4 @@
 node_js:
   - node
   - lts/*
+  - 6
diff --git a/resources/webidl2/CHANGELOG.md b/resources/webidl2/CHANGELOG.md
index e6fa641..1596d71 100644
--- a/resources/webidl2/CHANGELOG.md
+++ b/resources/webidl2/CHANGELOG.md
@@ -1,5 +1,73 @@
 # Change Log
 
+## [v10.2.0](https://github.com/w3c/webidl2.js/tree/v10.2.0) (2018-01-30)
+[Full Changelog](https://github.com/w3c/webidl2.js/compare/v10.1.0...v10.2.0)
+
+**Merged pull requests:**
+
+- Type on union idlType [\#135](https://github.com/w3c/webidl2.js/pull/135) ([saschanaz](https://github.com/saschanaz))
+- feat: add argument/return type [\#134](https://github.com/w3c/webidl2.js/pull/134) ([saschanaz](https://github.com/saschanaz))
+- feat: add dictionary/typedef-type [\#133](https://github.com/w3c/webidl2.js/pull/133) ([saschanaz](https://github.com/saschanaz))
+- feat: add const-type for idlTypes  [\#132](https://github.com/w3c/webidl2.js/pull/132) ([saschanaz](https://github.com/saschanaz))
+- feat: add types on idlTypes [\#131](https://github.com/w3c/webidl2.js/pull/131) ([saschanaz](https://github.com/saschanaz))
+- Auto acquisition for parser result changes [\#130](https://github.com/w3c/webidl2.js/pull/130) ([saschanaz](https://github.com/saschanaz))
+
+## [v10.1.0](https://github.com/w3c/webidl2.js/tree/v10.1.0) (2018-01-19)
+[Full Changelog](https://github.com/w3c/webidl2.js/compare/v10.0.0...v10.1.0)
+
+**Closed issues:**
+
+- Support `raises` and `setraises` [\#128](https://github.com/w3c/webidl2.js/issues/128)
+- Support `legacycaller` [\#127](https://github.com/w3c/webidl2.js/issues/127)
+- Improve "No semicolon after enum" message [\#119](https://github.com/w3c/webidl2.js/issues/119)
+
+**Merged pull requests:**
+
+- Let error messages include the current definition name [\#129](https://github.com/w3c/webidl2.js/pull/129) ([saschanaz](https://github.com/saschanaz))
+
+## [v10.0.0](https://github.com/w3c/webidl2.js/tree/v10.0.0) (2017-12-20)
+[Full Changelog](https://github.com/w3c/webidl2.js/compare/v9.0.0...v10.0.0)
+
+**Closed issues:**
+
+-  Always return an array for idlType, etc. [\#113](https://github.com/w3c/webidl2.js/issues/113)
+- Maintain writer.js or not? [\#109](https://github.com/w3c/webidl2.js/issues/109)
+
+**Merged pull requests:**
+
+- Remove typeExtAttrs from docs [\#124](https://github.com/w3c/webidl2.js/pull/124) ([saschanaz](https://github.com/saschanaz))
+- Remove iterator documentation [\#123](https://github.com/w3c/webidl2.js/pull/123) ([saschanaz](https://github.com/saschanaz))
+- Maintain writer.js [\#122](https://github.com/w3c/webidl2.js/pull/122) ([saschanaz](https://github.com/saschanaz))
+- BREAKING CHANGE: remove deprecated iterator operation [\#121](https://github.com/w3c/webidl2.js/pull/121) ([saschanaz](https://github.com/saschanaz))
+- Use for-of on tests [\#120](https://github.com/w3c/webidl2.js/pull/120) ([saschanaz](https://github.com/saschanaz))
+- docs\(README\): iterables ildType is always array [\#118](https://github.com/w3c/webidl2.js/pull/118) ([marcoscaceres](https://github.com/marcoscaceres))
+
+## [v9.0.0](https://github.com/w3c/webidl2.js/tree/v9.0.0) (2017-11-30)
+[Full Changelog](https://github.com/w3c/webidl2.js/compare/v8.1.0...v9.0.0)
+
+**Closed issues:**
+
+- Code quality [\#116](https://github.com/w3c/webidl2.js/issues/116)
+- Unable to parse HTMLAllCollection interface [\#114](https://github.com/w3c/webidl2.js/issues/114)
+- Add support for mixin syntax [\#112](https://github.com/w3c/webidl2.js/issues/112)
+- Whitespace issues [\#111](https://github.com/w3c/webidl2.js/issues/111)
+
+**Merged pull requests:**
+
+- Consistent array type for iterable.idlType [\#117](https://github.com/w3c/webidl2.js/pull/117) ([saschanaz](https://github.com/saschanaz))
+-  Revert "chore: drop Node 6 support \(\#102\)" [\#115](https://github.com/w3c/webidl2.js/pull/115) ([TimothyGu](https://github.com/TimothyGu))
+
+## [v8.1.0](https://github.com/w3c/webidl2.js/tree/v8.1.0) (2017-11-03)
+[Full Changelog](https://github.com/w3c/webidl2.js/compare/v8.0.1...v8.1.0)
+
+**Closed issues:**
+
+- Extended Attributes `rhs` should always be there [\#96](https://github.com/w3c/webidl2.js/issues/96)
+
+**Merged pull requests:**
+
+- Always add rhs property [\#110](https://github.com/w3c/webidl2.js/pull/110) ([saschanaz](https://github.com/saschanaz))
+
 ## [v8.0.1](https://github.com/w3c/webidl2.js/tree/v8.0.1) (2017-11-03)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v8.0.0...v8.0.1)
 
@@ -9,7 +77,7 @@
 
 **Merged pull requests:**
 
-- Remove m postfix from all\_ws\(\) [\#108](https://github.com/w3c/webidl2.js/pull/108) ([SaschaNaz](https://github.com/SaschaNaz))
+- Remove m postfix from all\_ws\(\) [\#108](https://github.com/w3c/webidl2.js/pull/108) ([saschanaz](https://github.com/saschanaz))
 
 ## [v8.0.0](https://github.com/w3c/webidl2.js/tree/v8.0.0) (2017-11-03)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v7.0.0...v8.0.0)
@@ -21,9 +89,9 @@
 
 **Merged pull requests:**
 
-- Support mixins + includes statements [\#105](https://github.com/w3c/webidl2.js/pull/105) ([SaschaNaz](https://github.com/SaschaNaz))
+- Support mixins + includes statements [\#105](https://github.com/w3c/webidl2.js/pull/105) ([saschanaz](https://github.com/saschanaz))
 - chore: drop Node 6 support [\#102](https://github.com/w3c/webidl2.js/pull/102) ([marcoscaceres](https://github.com/marcoscaceres))
-- BREAKING CHANGE: drop creator support [\#101](https://github.com/w3c/webidl2.js/pull/101) ([SaschaNaz](https://github.com/SaschaNaz))
+- BREAKING CHANGE: drop creator support [\#101](https://github.com/w3c/webidl2.js/pull/101) ([saschanaz](https://github.com/saschanaz))
 - Normalize some whitespace to pass wpt's lint [\#99](https://github.com/w3c/webidl2.js/pull/99) ([foolip](https://github.com/foolip))
 
 ## [v7.0.0](https://github.com/w3c/webidl2.js/tree/v7.0.0) (2017-10-27)
@@ -54,7 +122,7 @@
 
 **Merged pull requests:**
 
-- Use ES2015 syntax for tests [\#88](https://github.com/w3c/webidl2.js/pull/88) ([SaschaNaz](https://github.com/SaschaNaz))
+- Use ES2015 syntax for tests [\#88](https://github.com/w3c/webidl2.js/pull/88) ([saschanaz](https://github.com/saschanaz))
 
 ## [v6.0.0](https://github.com/w3c/webidl2.js/tree/v6.0.0) (2017-10-17)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v5.0.0...v6.0.0)
@@ -73,7 +141,7 @@
 
 **Merged pull requests:**
 
-- BREAKING CHANGE: Use ES2015 syntax [\#84](https://github.com/w3c/webidl2.js/pull/84) ([SaschaNaz](https://github.com/SaschaNaz))
+- BREAKING CHANGE: Use ES2015 syntax [\#84](https://github.com/w3c/webidl2.js/pull/84) ([saschanaz](https://github.com/saschanaz))
 
 ## [v4.2.0](https://github.com/w3c/webidl2.js/tree/v4.2.0) (2017-10-16)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v4.1.0...v4.2.0)
@@ -85,8 +153,8 @@
 
 **Merged pull requests:**
 
-- Check duplicated names [\#80](https://github.com/w3c/webidl2.js/pull/80) ([SaschaNaz](https://github.com/SaschaNaz))
-- Remove legacycaller [\#79](https://github.com/w3c/webidl2.js/pull/79) ([SaschaNaz](https://github.com/SaschaNaz))
+- Check duplicated names [\#80](https://github.com/w3c/webidl2.js/pull/80) ([saschanaz](https://github.com/saschanaz))
+- Remove legacycaller [\#79](https://github.com/w3c/webidl2.js/pull/79) ([saschanaz](https://github.com/saschanaz))
 - Add "sequence" property to IDL Type AST definition [\#76](https://github.com/w3c/webidl2.js/pull/76) ([lerouche](https://github.com/lerouche))
 
 ## [v4.1.0](https://github.com/w3c/webidl2.js/tree/v4.1.0) (2017-07-04)
@@ -98,7 +166,7 @@
 
 **Merged pull requests:**
 
-- Support TypeWithExtendedAttributes on generics [\#75](https://github.com/w3c/webidl2.js/pull/75) ([SaschaNaz](https://github.com/SaschaNaz))
+- Support TypeWithExtendedAttributes on generics [\#75](https://github.com/w3c/webidl2.js/pull/75) ([saschanaz](https://github.com/saschanaz))
 
 ## [v4.0.0](https://github.com/w3c/webidl2.js/tree/v4.0.0) (2017-06-27)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v3.0.2...v4.0.0)
@@ -112,7 +180,7 @@
 **Merged pull requests:**
 
 - BREAKING CHANGE: remove serializers \(closes \#73\) [\#74](https://github.com/w3c/webidl2.js/pull/74) ([marcoscaceres](https://github.com/marcoscaceres))
-- Add documentation for namespaces [\#70](https://github.com/w3c/webidl2.js/pull/70) ([SaschaNaz](https://github.com/SaschaNaz))
+- Add documentation for namespaces [\#70](https://github.com/w3c/webidl2.js/pull/70) ([saschanaz](https://github.com/saschanaz))
 
 ## [v3.0.2](https://github.com/w3c/webidl2.js/tree/v3.0.2) (2017-05-29)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v3.0.1...v3.0.2)
@@ -123,7 +191,7 @@
 
 **Merged pull requests:**
 
-- Test for latest LTS/stable node versions [\#69](https://github.com/w3c/webidl2.js/pull/69) ([SaschaNaz](https://github.com/SaschaNaz))
+- Test for latest LTS/stable node versions [\#69](https://github.com/w3c/webidl2.js/pull/69) ([saschanaz](https://github.com/saschanaz))
 
 ## [v3.0.1](https://github.com/w3c/webidl2.js/tree/v3.0.1) (2017-05-18)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v2.4.0...v3.0.1)
@@ -135,8 +203,8 @@
 
 **Merged pull requests:**
 
-- Fix whitespace error on parsing extended attributes [\#68](https://github.com/w3c/webidl2.js/pull/68) ([SaschaNaz](https://github.com/SaschaNaz))
-- Remove deprecated IDL arrays and exceptions [\#67](https://github.com/w3c/webidl2.js/pull/67) ([SaschaNaz](https://github.com/SaschaNaz))
+- Fix whitespace error on parsing extended attributes [\#68](https://github.com/w3c/webidl2.js/pull/68) ([saschanaz](https://github.com/saschanaz))
+- Remove deprecated IDL arrays and exceptions [\#67](https://github.com/w3c/webidl2.js/pull/67) ([saschanaz](https://github.com/saschanaz))
 
 ## [v2.4.0](https://github.com/w3c/webidl2.js/tree/v2.4.0) (2017-04-12)
 [Full Changelog](https://github.com/w3c/webidl2.js/compare/v2.1.0...v2.4.0)
@@ -157,7 +225,7 @@
 - Update webidl2.js [\#63](https://github.com/w3c/webidl2.js/pull/63) ([tqeto](https://github.com/tqeto))
 - Remove support for MapClass \(no longer valid in WebIDL\) [\#62](https://github.com/w3c/webidl2.js/pull/62) ([dontcallmedom](https://github.com/dontcallmedom))
 - Add support for annotated types [\#61](https://github.com/w3c/webidl2.js/pull/61) ([dontcallmedom](https://github.com/dontcallmedom))
-- Support namespaces [\#58](https://github.com/w3c/webidl2.js/pull/58) ([SaschaNaz](https://github.com/SaschaNaz))
+- Support namespaces [\#58](https://github.com/w3c/webidl2.js/pull/58) ([saschanaz](https://github.com/saschanaz))
 - Add support for records [\#57](https://github.com/w3c/webidl2.js/pull/57) ([TimothyGu](https://github.com/TimothyGu))
 - Refactor [\#50](https://github.com/w3c/webidl2.js/pull/50) ([marcoscaceres](https://github.com/marcoscaceres))
 - feat\(lib\): add AMD export support \(closes \#48\) [\#49](https://github.com/w3c/webidl2.js/pull/49) ([marcoscaceres](https://github.com/marcoscaceres))
diff --git a/resources/webidl2/README.md b/resources/webidl2/README.md
index 93cc78b..8791360 100644
--- a/resources/webidl2/README.md
+++ b/resources/webidl2/README.md
@@ -310,7 +310,6 @@
 ```JS
 {
   "type": "typedef",
-  "typeExtAttrs": [],
   "idlType": {
     "sequence": true,
     "generic": "sequence",
@@ -336,8 +335,6 @@
 * `name`: The typedef's name.
 * `idlType`: An [IDL Type](#idl-type) describing what typedef's type.
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
-* `typeExtAttrs`: A list of [extended attributes](#extended-attributes) that apply to the
-type rather than to the typedef as a whole.
 
 ### Implements
 
@@ -489,34 +486,6 @@
 * `value`: The constant value as described by [Const Values](#default-and-const-values)
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
 
-### Iterator Member
-
-Iterator members look like this
-
-```JS
-{
-  "type": "iterator",
-  "getter": false,
-  "setter": false,
-  "deleter": false,
-  "static": false,
-  "stringifier": false,
-  "idlType": {
-    "sequence": false,
-    "generic": null,
-    "nullable": false,
-    "union": false,
-    "idlType": "Session2"
-  },
-  "iteratorObject": "SessionIterator",
-  "extAttrs": []
-}
-```
-
-* `type`: Always "iterator".
-* `iteratorObject`: The string on the right-hand side; absent if there isn't one.
-* the rest: same as on [operations](#operation-member).
-
 ### Arguments
 
 The arguments (e.g. for an operation) look like this:
@@ -611,7 +580,7 @@
 The fields are as follows:
 
 * `type`: Always one of "iterable", "legacyiterable", "maplike" or "setlike".
-* `idlType`: An [IDL Type](#idl-type) (or an array of two types) representing the declared type arguments.
+* `idlType`: An array with one or more [IDL Types](#idl-type) representing the declared type arguments.
 * `readonly`: Whether the maplike or setlike is declared as read only.
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
 
diff --git a/resources/webidl2/lib/webidl2.js b/resources/webidl2/lib/webidl2.js
index 0c9a1fa..a7a61d9 100644
--- a/resources/webidl2/lib/webidl2.js
+++ b/resources/webidl2/lib/webidl2.js
@@ -1,33 +1,82 @@
 "use strict";
 
 (() => {
+  // These regular expressions use the sticky flag so they will only match at
+  // the current location (ie. the offset of lastIndex).
+  const tokenRe = {
+    // This expression uses a lookahead assertion to catch false matches
+    // against integers early.
+    "float": /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y,
+    "integer": /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y,
+    "identifier": /[A-Z_a-z][0-9A-Z_a-z-]*/y,
+    "string": /"[^"]*"/y,
+    "whitespace": /[\t\n\r ]+/y,
+    "comment": /((\/(\/.*|\*([^*]|\*[^\/])*\*\/)[\t\n\r ]*)+)/y,
+    "other": /[^\t\n\r 0-9A-Z_a-z]/y
+  };
+
+  function attemptTokenMatch(str, type, re, lastIndex, tokens) {
+    re.lastIndex = lastIndex;
+    const result = re.exec(str);
+    if (result) {
+      tokens.push({ type, value: result[0] });
+      return re.lastIndex;
+    }
+    return -1;
+  }
+
   function tokenise(str) {
     const tokens = [];
-    const re = {
-      "float": /^-?(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/,
-      "integer": /^-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/,
-      "identifier": /^[A-Z_a-z][0-9A-Z_a-z-]*/,
-      "string": /^"[^"]*"/,
-      "whitespace": /^(?:[\t\n\r ]+|[\t\n\r ]*((\/\/.*|\/\*(.|\n|\r)*?\*\/)[\t\n\r ]*))+/,
-      "other": /^[^\t\n\r 0-9A-Z_a-z]/
-    };
-    const types = ["float", "integer", "identifier", "string", "whitespace", "other"];
-    while (str.length > 0) {
-      let matched = false;
-      for (var i in types) {
-        const type = types[i];
-        str = str.replace(re[type], tok => {
-          tokens.push({ type, value: tok });
-          matched = true;
-          return "";
-        });
-        if (matched) break;
+    let lastIndex = 0;
+    while (lastIndex < str.length) {
+      const nextChar = str.charAt(lastIndex);
+      let result = -1;
+      if (/[-0-9.]/.test(nextChar)) {
+        result = attemptTokenMatch(str, "float", tokenRe.float, lastIndex,
+                                   tokens);
+        if (result === -1) {
+          result = attemptTokenMatch(str, "integer", tokenRe.integer, lastIndex,
+                                     tokens);
+        }
+        if (result === -1) {
+          // '-' and '.' can also match "other".
+          result = attemptTokenMatch(str, "other", tokenRe.other,
+                                     lastIndex, tokens);
+        }
+      } else if (/[A-Z_a-z]/.test(nextChar)) {
+        result = attemptTokenMatch(str, "identifier", tokenRe.identifier,
+                                   lastIndex, tokens);
+      } else if (nextChar === '"') {
+        result = attemptTokenMatch(str, "string", tokenRe.string,
+                                   lastIndex, tokens);
+        if (result === -1) {
+          // '"' can also match "other".
+          result = attemptTokenMatch(str, "other", tokenRe.other,
+                                     lastIndex, tokens);
+        }
+      } else if (/[\t\n\r ]/.test(nextChar)) {
+        result = attemptTokenMatch(str, "whitespace", tokenRe.whitespace,
+                                   lastIndex, tokens);
+      } else if (nextChar === '/') {
+        // The parser expects comments to be labelled as "whitespace".
+        result = attemptTokenMatch(str, "whitespace", tokenRe.comment,
+                                   lastIndex, tokens);
+        if (result === -1) {
+          // '/' can also match "other".
+          result = attemptTokenMatch(str, "other", tokenRe.other,
+                                     lastIndex, tokens);
+        }
+      } else {
+        result = attemptTokenMatch(str, "other", tokenRe.other,
+                                   lastIndex, tokens);
       }
-      if (matched) continue;
-      throw new Error("Token stream not progressing");
+      if (result === -1) {
+        throw new Error("Token stream not progressing");
+      }
+      lastIndex = result;
     }
     return tokens;
-  };
+  }
 
   class WebIDLParseError {
     constructor(str, line, input, tokens) {
@@ -46,6 +95,7 @@
     let line = 1;
     tokens = tokens.slice();
     const names = new Map();
+    let current = null;
 
     const FLOAT = "float";
     const INT = "integer";
@@ -58,7 +108,7 @@
       getter: false,
       setter: false,
       deleter: false,
-      "static": false,
+      static: false,
       stringifier: false
     });
 
@@ -70,8 +120,18 @@
         tok += tokens[numTokens].value;
         numTokens++;
       }
-      throw new WebIDLParseError(str, line, tok, tokens.slice(0, maxTokens));
-    };
+      
+      let message;
+      if (current) {
+        message = `Got an error during or right after parsing \`${current.partial ? "partial " : ""}${current.type} ${current.name}\`: ${str}`
+      }
+      else {
+        // throwing before any valid definition
+        message = `Got an error before parsing any named definition: ${str}`;
+      }
+
+      throw new WebIDLParseError(message, line, tok, tokens.slice(0, maxTokens));
+    }
 
     function sanitize_name(name, type) {
       if (names.has(name)) {
@@ -87,23 +147,34 @@
       if (!tokens.length || tokens[0].type !== type) return;
       if (typeof value === "undefined" || tokens[0].value === value) {
         last_token = tokens.shift();
-        if (type === ID) last_token.value = last_token.value.replace(/^_/, "");
+        if (type === ID && last_token.value.startsWith('_'))
+          last_token.value = last_token.value.substring(1);
         return last_token;
       }
-    };
+    }
+
+    function count(str, char) {
+      let total = 0;
+      for (let i = str.indexOf(char); i !== -1; i = str.indexOf(char, i + 1)) {
+        ++total;
+      }
+      return total;
+    }
 
     function ws() {
       if (!tokens.length) return;
       if (tokens[0].type === "whitespace") {
         const t = tokens.shift();
-        t.value.replace(/\n/g, m => {
-          line++;
-          return m;
-        });
+        line += count(t.value, '\n');
         return t;
       }
-    };
+    }
 
+    const all_ws_re = {
+      "ws": /([\t\n\r ]+)/y,
+      "line-comment": /\/\/(.*)\r?\n?/y,
+      "multiline-comment": /\/\*((?:[^*]|\*[^/])*)\*\//y
+    };
     function all_ws(store, pea) { // pea == post extended attribute, tpea = same for types
       const t = { type: "whitespace", value: "" };
       while (true) {
@@ -114,31 +185,30 @@
       if (t.value.length > 0) {
         if (store) {
           let w = t.value;
-          const re = {
-            "ws": /^([\t\n\r ]+)/,
-            "line-comment": /^\/\/(.*)\r?\n?/,
-            "multiline-comment": /^\/\*((?:.|\n|\r)*?)\*\//
-          };
-          const wsTypes = [];
-          for (var k in re) wsTypes.push(k);
-          while (w.length) {
+          let lastIndex = 0;
+          while (lastIndex < w.length) {
             let matched = false;
-            for (var i in wsTypes) {
-              const type = wsTypes[i];
-              w = w.replace(re[type], (tok, m1) => {
-                store.push({ type: type + (pea ? ("-" + pea) : ""), value: m1 });
+            // Servo doesn't support using "const" in this construction yet.
+            // See https://github.com/servo/servo/issues/20231.
+            // |type| can be made const once Servo supports it.
+            for (let type in all_ws_re) {
+              const re = all_ws_re[type];
+              re.lastIndex = lastIndex;
+              const result = re.exec(w);
+              if (result) {
+                store.push({ type: type + (pea ? ("-" + pea) : ""), value: result[1] });
                 matched = true;
-                return "";
-              });
-              if (matched) break;
+                lastIndex = re.lastIndex;
+                break;
+              }
             }
-            if (matched) continue;
-            throw new Error("Surprising white space construct."); // this shouldn't happen
+            if (!matched)
+              throw new Error("Surprising white space construct."); // this shouldn't happen
           }
         }
         return t;
       }
-    };
+    }
 
     function integer_type() {
       let ret = "";
@@ -153,7 +223,7 @@
         return ret;
       }
       if (ret) error("Failed to parse integer type");
-    };
+    }
 
     function float_type() {
       let ret = "";
@@ -163,7 +233,7 @@
       if (consume(ID, "float")) return ret + "float";
       if (consume(ID, "double")) return ret + "double";
       if (ret) error("Failed to parse float type");
-    };
+    }
 
     function primitive_type() {
       const num_type = integer_type() || float_type();
@@ -172,7 +242,7 @@
       if (consume(ID, "boolean")) return "boolean";
       if (consume(ID, "byte")) return "byte";
       if (consume(ID, "octet")) return "octet";
-    };
+    }
 
     function const_value() {
       if (consume(ID, "true")) return { type: "boolean", value: true };
@@ -187,7 +257,7 @@
         if (consume(ID, "Infinity")) return { type: "Infinity", negative: true };
         else tokens.unshift(tok);
       }
-    };
+    }
 
     function type_suffix(obj) {
       while (true) {
@@ -197,11 +267,11 @@
           obj.nullable = true;
         } else return;
       }
-    };
+    }
 
-    function single_type() {
+    function single_type(typeName) {
       const prim = primitive_type();
-      const ret = { sequence: false, generic: null, nullable: false, union: false };
+      const ret = { type: typeName || null, sequence: false, generic: null, nullable: false, union: false };
       let name;
       let value;
       if (prim) {
@@ -219,7 +289,7 @@
           const types = [];
           do {
             all_ws();
-            types.push(type_with_extended_attributes() || error("Error parsing generic type " + value));
+            types.push(type_with_extended_attributes(typeName) || error("Error parsing generic type " + value));
             all_ws();
           }
           while (consume(OTHER, ","));
@@ -248,12 +318,12 @@
       type_suffix(ret);
       if (ret.nullable && ret.idlType === "any") error("Type any cannot be made nullable");
       return ret;
-    };
+    }
 
-    function union_type() {
+    function union_type(typeName) {
       all_ws();
       if (!consume(OTHER, "(")) return;
-      const ret = { sequence: false, generic: null, nullable: false, union: true, idlType: [] };
+      const ret = { type: typeName || null, sequence: false, generic: null, nullable: false, union: true, idlType: [] };
       const fst = type_with_extended_attributes() || error("Union type with no content");
       ret.idlType.push(fst);
       while (true) {
@@ -265,18 +335,18 @@
       if (!consume(OTHER, ")")) error("Unterminated union type");
       type_suffix(ret);
       return ret;
-    };
+    }
 
-    function type() {
-      return single_type() || union_type();
-    };
+    function type(typeName) {
+      return single_type(typeName) || union_type(typeName);
+    }
 
-    function type_with_extended_attributes() {
+    function type_with_extended_attributes(typeName) {
       const extAttrs = extended_attrs();
-      const ret = single_type() || union_type();
+      const ret = single_type(typeName) || union_type(typeName);
       if (extAttrs.length && ret) ret.extAttrs = extAttrs;
       return ret;
-    };
+    }
 
     function argument(store) {
       const ret = { optional: false, variadic: false };
@@ -287,7 +357,7 @@
         ret.optional = true;
         all_ws();
       }
-      ret.idlType = type_with_extended_attributes();
+      ret.idlType = type_with_extended_attributes("argument-type");
       if (!ret.idlType) {
         if (opt_token) tokens.unshift(opt_token);
         return;
@@ -322,7 +392,7 @@
         }
       }
       return ret;
-    };
+    }
 
     function argument_list(store) {
       const ret = [];
@@ -335,7 +405,7 @@
         const nxt = argument(store ? ret : null) || error("Trailing comma in arguments list");
         ret.push(nxt);
       }
-    };
+    }
 
     function simple_extended_attr(store) {
       all_ws();
@@ -386,7 +456,7 @@
         consume(OTHER, ")") || error("Unexpected token in extended attribute argument list");
       }
       return ret;
-    };
+    }
 
     // Note: we parse something simpler than the official syntax. It's all that ever
     // seems to be used
@@ -406,7 +476,7 @@
       all_ws();
       consume(OTHER, "]") || error("No end of extended attribute");
       return eas;
-    };
+    }
 
     function default_() {
       all_ws();
@@ -420,11 +490,11 @@
           return { type: "sequence", value: [] };
         } else {
           const str = consume(STR) || error("No value for default");
-          str.value = str.value.replace(/^"/, "").replace(/"$/, "");
+          str.value = str.value.slice(1, -1);
           return str;
         }
       }
-    };
+    }
 
     function const_(store) {
       all_ws(store, "pea");
@@ -436,7 +506,7 @@
         typ = consume(ID) || error("No type for const");
         typ = typ.value;
       }
-      ret.idlType = typ;
+      ret.idlType = { type: "const-type", idlType: typ };
       all_ws();
       if (consume(OTHER, "?")) {
         ret.nullable = true;
@@ -453,7 +523,7 @@
       all_ws();
       consume(OTHER, ";") || error("Unterminated const");
       return ret;
-    };
+    }
 
     function inheritance() {
       all_ws();
@@ -462,7 +532,7 @@
         const inh = consume(ID) || error("No type in inheritance");
         return inh.value;
       }
-    };
+    }
 
     function operation_rest(ret, store) {
       all_ws();
@@ -477,7 +547,7 @@
       all_ws();
       consume(OTHER, ";") || error("Unterminated operation");
       return ret;
-    };
+    }
 
     function callback(store) {
       all_ws(store, "pea");
@@ -486,12 +556,11 @@
       all_ws();
       const tok = consume(ID, "interface");
       if (tok) {
-        ret = interface_rest();
-        ret.type = "callback interface";
+        ret = interface_rest(false, store, "callback interface");
         return ret;
       }
       const name = consume(ID) || error("No name for callback");
-      ret = { type: "callback", name: sanitize_name(name.value, "callback") };
+      ret = current = { type: "callback", name: sanitize_name(name.value, "callback") };
       all_ws();
       consume(OTHER, "=") || error("No assignment in callback");
       all_ws();
@@ -504,14 +573,14 @@
       all_ws();
       consume(OTHER, ";") || error("Unterminated callback");
       return ret;
-    };
+    }
 
     function attribute(store) {
       all_ws(store, "pea");
       const grabbed = [];
       const ret = {
         type: "attribute",
-        "static": false,
+        static: false,
         stringifier: false,
         inherit: false,
         readonly: false
@@ -519,7 +588,7 @@
       const w = all_ws();
       if (w) grabbed.push(w);
       if (consume(ID, "inherit")) {
-        if (ret["static"] || ret.stringifier) error("Cannot have a static or stringifier inherit");
+        if (ret.static || ret.stringifier) error("Cannot have a static or stringifier inherit");
         ret.inherit = true;
         grabbed.push(last_token);
         const w = all_ws();
@@ -536,14 +605,14 @@
         tokens = grabbed.concat(tokens);
       }
       return rest;
-    };
+    }
 
     function attribute_rest(ret) {
       if (!consume(ID, "attribute")) {
         return;
       }
       all_ws();
-      ret.idlType = type_with_extended_attributes() || error("No type in attribute");
+      ret.idlType = type_with_extended_attributes("attribute-type") || error("No type in attribute");
       if (ret.idlType.sequence) error("Attributes cannot accept sequence types");
       if (ret.idlType.generic === "record") error("Attributes cannot accept record types");
       all_ws();
@@ -552,17 +621,17 @@
       all_ws();
       consume(OTHER, ";") || error("Unterminated attribute");
       return ret;
-    };
+    }
 
     function return_type() {
-      const typ = type();
+      const typ = type("return-type");
       if (!typ) {
         if (consume(ID, "void")) {
           return "void";
         } else error("No return type");
       }
       return typ;
-    };
+    }
 
     function operation(store) {
       all_ws(store, "pea");
@@ -582,24 +651,9 @@
       }
       ret.idlType = return_type();
       all_ws();
-      if (consume(ID, "iterator")) {
-        all_ws();
-        ret.type = "iterator";
-        if (consume(ID, "object")) {
-          ret.iteratorObject = "object";
-        } else if (consume(OTHER, "=")) {
-          all_ws();
-          var name = consume(ID) || error("No right hand side in iterator");
-          ret.iteratorObject = name.value;
-        }
-        all_ws();
-        consume(OTHER, ";") || error("Unterminated iterator");
-        return ret;
-      } else {
-        operation_rest(ret, store);
-        return ret;
-      }
-    };
+      operation_rest(ret, store);
+      return ret;
+    }
 
     function static_member(store) {
       all_ws(store, "pea");
@@ -631,7 +685,7 @@
           arr.push(name.value);
         } else break;
       }
-    };
+    }
 
     function iterable_type() {
       if (consume(ID, "iterable")) return "iterable";
@@ -639,13 +693,13 @@
       else if (consume(ID, "maplike")) return "maplike";
       else if (consume(ID, "setlike")) return "setlike";
       else return;
-    };
+    }
 
     function readonly_iterable_type() {
       if (consume(ID, "maplike")) return "maplike";
       else if (consume(ID, "setlike")) return "setlike";
       else return;
-    };
+    }
 
     function iterable(store) {
       all_ws(store, "pea");
@@ -672,17 +726,14 @@
         delete ret.readonly;
       all_ws();
       if (consume(OTHER, "<")) {
-        ret.idlType = type_with_extended_attributes() || error(`Error parsing ${ittype} declaration`);
+        ret.idlType = [type_with_extended_attributes()] || error(`Error parsing ${ittype} declaration`);
         all_ws();
         if (secondTypeAllowed) {
-          let type2 = null;
           if (consume(OTHER, ",")) {
             all_ws();
-            type2 = type_with_extended_attributes();
+            ret.idlType.push(type_with_extended_attributes());
             all_ws();
           }
-          if (type2)
-            ret.idlType = [ret.idlType, type2];
           else if (secondTypeRequired)
             error(`Missing second type argument in ${ittype} declaration`);
         }
@@ -693,16 +744,16 @@
         error(`Error parsing ${ittype} declaration`);
 
       return ret;
-    };
+    }
 
-    function interface_rest(isPartial, store) {
+    function interface_rest(isPartial, store, typeName = "interface") {
       all_ws();
       const name = consume(ID) || error("No name for interface");
       const mems = [];
-      const ret = {
-        type: "interface",
+      const ret = current = {
+        type: typeName,
         name: isPartial ? name.value : sanitize_name(name.value, "interface"),
-        partial: false,
+        partial: isPartial,
         members: mems
       };
       if (!isPartial) ret.inheritance = inheritance() || null;
@@ -733,7 +784,7 @@
         mem.extAttrs = ea;
         ret.members.push(mem);
       }
-    };
+    }
 
     function mixin_rest(isPartial, store) {
       all_ws();
@@ -741,10 +792,10 @@
       all_ws();
       const name = consume(ID) || error("No name for interface mixin");
       const mems = [];
-      const ret = {
+      const ret = current = {
         type: "interface mixin",
         name: isPartial ? name.value : sanitize_name(name.value, "interface mixin"),
-        partial: false,
+        partial: isPartial,
         members: mems
       };
       all_ws();
@@ -787,7 +838,7 @@
       all_ws();
       const name = consume(ID) || error("No name for namespace");
       const mems = [];
-      const ret = {
+      const ret = current = {
         type: "namespace",
         name: isPartial ? name.value : sanitize_name(name.value, "namespace"),
         partial: isPartial,
@@ -817,7 +868,7 @@
       const grabbed = [];
       const ret = {
         type: "attribute",
-        "static": false,
+        static: false,
         stringifier: false,
         inherit: false,
         readonly: false
@@ -856,9 +907,8 @@
         interface_(true, store) ||
         namespace(true, store) ||
         error("Partial doesn't apply to anything");
-      thing.partial = true;
       return thing;
-    };
+    }
 
     function dictionary(isPartial, store) {
       all_ws(isPartial ? null : store, "pea");
@@ -866,10 +916,10 @@
       all_ws();
       const name = consume(ID) || error("No name for dictionary");
       const mems = [];
-      const ret = {
+      const ret = current = {
         type: "dictionary",
         name: isPartial ? name.value : sanitize_name(name.value, "dictionary"),
-        partial: false,
+        partial: isPartial,
         members: mems
       };
       if (!isPartial) ret.inheritance = inheritance() || null;
@@ -885,7 +935,7 @@
         const ea = extended_attrs(store ? mems : null);
         all_ws(store ? mems : null, "pea");
         const required = consume(ID, "required");
-        const typ = type_with_extended_attributes() || error("No type for dictionary member");
+        const typ = type_with_extended_attributes("dictionary-type") || error("No type for dictionary member");
         all_ws();
         const name = consume(ID) || error("No name for dictionary member");
         const dflt = default_();
@@ -904,7 +954,7 @@
         all_ws();
         consume(OTHER, ";") || error("Unterminated dictionary member");
       }
-    };
+    }
 
     function enum_(store) {
       all_ws(store, "pea");
@@ -912,7 +962,7 @@
       all_ws();
       const name = consume(ID) || error("No name for enum");
       const vals = [];
-      const ret = {
+      const ret = current = {
         type: "enum",
         name: sanitize_name(name.value, "enum"),
         values: vals
@@ -928,7 +978,7 @@
           return ret;
         }
         const val = consume(STR) || error("Unexpected value in enum");
-        val.value = val.value.replace(/"/g, "");
+        val.value = val.value.slice(1, -1);
         ret.values.push(val);
         all_ws(store ? vals : null);
         if (consume(OTHER, ",")) {
@@ -939,7 +989,7 @@
           saw_comma = false;
         }
       }
-    };
+    }
 
     function typedef(store) {
       all_ws(store, "pea");
@@ -948,14 +998,15 @@
         type: "typedef"
       };
       all_ws();
-      ret.idlType = type_with_extended_attributes() || error("No type in typedef");
+      ret.idlType = type_with_extended_attributes("typedef-type") || error("No type in typedef");
       all_ws();
       const name = consume(ID) || error("No name in typedef");
       ret.name = sanitize_name(name.value, "typedef");
+      current = ret;
       all_ws();
       consume(OTHER, ";") || error("Unterminated typedef");
       return ret;
-    };
+    }
 
     function implements_(store) {
       all_ws(store, "pea");
@@ -978,7 +1029,7 @@
         tokens.unshift(w);
         tokens.unshift(target);
       }
-    };
+    }
 
     function includes(store) {
       all_ws(store, "pea");
@@ -1001,7 +1052,7 @@
         tokens.unshift(w);
         tokens.unshift(target);
       }
-    };
+    }
 
     function definition(store) {
       return callback(store) ||
@@ -1013,7 +1064,7 @@
         implements_(store) ||
         includes(store) ||
         namespace(false, store);
-    };
+    }
 
     function definitions(store) {
       if (!tokens.length) return [];
@@ -1029,11 +1080,11 @@
         defs.push(def);
       }
       return defs;
-    };
+    }
     const res = definitions(opt.ws);
     if (tokens.length) error("Unrecognised tokens");
     return res;
-  };
+  }
 
   const obj = {
     parse(str, opt) {
diff --git a/resources/webidl2/lib/writer.js b/resources/webidl2/lib/writer.js
index 5e30e70..c00c0dd 100644
--- a/resources/webidl2/lib/writer.js
+++ b/resources/webidl2/lib/writer.js
@@ -1,78 +1,71 @@
-(function() {
+"use strict";
 
-  var write = function(ast, opt) {
-    var curPea = "",
-      curTPea = "",
-      opt = opt || {},
-      noop = function(str) {
-        return str; },
-      optNames = "type".split(" "),
-      context = [];
-    for (var i = 0, n = optNames.length; i < n; i++) {
-      var o = optNames[i];
+(() => {
+  function write(ast, opt = {}) {
+    let curPea = "";
+    let curTPea = "";
+    const noop = str => str;
+    const optNames = "type".split(" ");
+    const context = [];
+    for (const o of optNames) {
       if (!opt[o]) opt[o] = noop;
     }
 
-    var literal = function(it) {
+    function literal(it) {
       return it.value;
     };
-    var wsPea = function(it) {
+    function wsPea(it) {
       curPea += it.value;
       return "";
     };
-    var wsTPea = function(it) {
+    function wsTPea(it) {
       curTPea += it.value;
       return "";
     };
-    var lineComment = function(it) {
-      return "//" + it.value + "\n";
+    function lineComment(it) {
+      return `//${it.value}\n`;
     };
-    var multilineComment = function(it) {
-      return "/*" + it.value + "*/";
+    function multilineComment(it) {
+      return `/*${it.value}*/`;
     };
-    var type = function(it) {
+    function type(it) {
       if (typeof it === "string") return opt.type(it); // XXX should maintain some context
-      if (it.union) return "(" + it.idlType.map(type).join(" or ") + ")";
-      var ret = "";
-      if (it.generic) ret += it.generic + "<";
-      else if (it.sequence) ret += "sequence<";
-      if (Array.isArray(it.idlType)) ret += it.idlType.map(type).join(", ");
-      else ret += type(it.idlType);
-      if (it.array || it.generic === 'Array') {
-        for (var i = 0, n = it.nullableArray.length; i < n; i++) {
-          var val = it.nullableArray[i];
-          if (val) ret += "?";
-          ret += "[]";
-        }
+      let ret = extended_attributes(it.extAttrs, curPea);
+      if (it.union) ret += `(${it.idlType.map(type).join(" or ")})`;
+      else {
+        if (it.generic) ret += `${it.generic}<`;
+        if (Array.isArray(it.idlType)) ret += it.idlType.map(type).join(", ");
+        else ret += type(it.idlType);
+        if (it.generic) ret += ">";
       }
-      if (it.generic || it.sequence) ret += ">";
       if (it.nullable) ret += "?";
 
       return ret;
     };
-    var const_value = function(it) {
-      var tp = it.type;
+    function const_value(it) {
+      const tp = it.type;
       if (tp === "boolean") return it.value ? "true" : "false";
       else if (tp === "null") return "null";
       else if (tp === "Infinity") return (it.negative ? "-" : "") + "Infinity";
       else if (tp === "NaN") return "NaN";
       else if (tp === "number") return it.value;
-      else return '"' + it.value + '"';
+      else if (tp === "sequence") return "[]";
+      else return `"${it.value}"`;
     };
-    var argument = function(arg, pea) {
-      var ret = extended_attributes(arg.extAttrs, pea);
+    function argument(arg, pea) {
+      let ret = extended_attributes(arg.extAttrs, pea);
       if (arg.optional) ret += "optional ";
       ret += type(arg.idlType);
       if (arg.variadic) ret += "...";
-      ret += " " + arg.name;
-      if (arg["default"]) ret += " = " + const_value(arg["default"]);
+      ret += ` ${arg.name}`;
+      if (arg["default"]) ret += ` = ${const_value(arg["default"])}`;
       return ret;
     };
-    var args = function(its) {
-      var res = "",
-        pea = "";
-      for (var i = 0, n = its.length; i < n; i++) {
-        var arg = its[i];
+    function args(its) {
+      let res = "";
+      let pea = "";
+      for (let i = 0, n = its.length; i < n; i++) {
+        const arg = its[i];
         if (arg.type === "ws") res += arg.value;
         else if (arg.type === "ws-pea") pea += arg.value;
         else {
@@ -83,182 +76,198 @@
       }
       return res;
     };
-    var make_ext_at = function(it) {
-      if (it["arguments"] === null) return it.name;
+    function make_ext_at(it) {
       context.unshift(it);
-      var ret = it.name + "(" + (it["arguments"].length ? args(it["arguments"]) : "") + ")";
+      let ret = it.name;
+      if (it.rhs) {
+        if (it.rhs.type === "identifier-list") ret += `=(${it.rhs.value.join(',')})`;
+        else ret += `=${it.rhs.value}`;
+      }
+      if (it.arguments) ret += `(${it["arguments"].length ? args(it["arguments"]) : ""})`;
       context.shift(); // XXX need to add more contexts, but not more than needed for ReSpec
       return ret;
     };
-    var extended_attributes = function(eats, pea) {
+    function extended_attributes(eats, pea) {
       if (!eats || !eats.length) return "";
-      return "[" + eats.map(make_ext_at).join(", ") + "]" + pea;
+      return `[${eats.map(make_ext_at).join(", ")}]${pea}`;
     };
 
-    var modifiers = "getter setter creator deleter legacycaller stringifier static".split(" ");
-    var operation = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    const modifiers = "getter setter creator deleter legacycaller stringifier static".split(" ");
+    function operation(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
       if (it.stringifier && !it.idlType) return "stringifier;";
-      for (var i = 0, n = modifiers.length; i < n; i++) {
-        var mod = modifiers[i];
+      for (const mod of modifiers) {
         if (it[mod]) ret += mod + " ";
       }
       ret += type(it.idlType) + " ";
       if (it.name) ret += it.name;
-      ret += "(" + args(it["arguments"]) + ");";
+      ret += `(${args(it["arguments"])});`;
       return ret;
     };
 
-    var attribute = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function attribute(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
       if (it["static"]) ret += "static ";
       if (it.stringifier) ret += "stringifier ";
       if (it.readonly) ret += "readonly ";
       if (it.inherit) ret += "inherit ";
-      ret += "attribute " + type(it.idlType) + " " + it.name + ";";
+      ret += `attribute ${type(it.idlType)} ${it.name};`;
       return ret;
     };
 
-    var interface_ = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function interface_(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
       if (it.partial) ret += "partial ";
-      ret += "interface " + it.name + " ";
-      if (it.inheritance) ret += ": " + it.inheritance + " ";
-      ret += "{" + iterate(it.members) + "};";
+      ret += `interface ${it.name} `;
+      if (it.inheritance) ret += `: ${it.inheritance} `;
+      ret += `{${iterate(it.members)}};`;
       return ret;
     };
 
-    var dictionary = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function interface_mixin(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
       if (it.partial) ret += "partial ";
-      ret += "dictionary " + it.name + " ";
-      ret += "{" + iterate(it.members) + "};";
+      ret += `interface mixin ${it.name} `;
+      ret += `{${iterate(it.members)}};`;
+      return ret;
+    }
+
+    function namespace(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
+      curPea = "";
+      if (it.partial) ret += "partial ";
+      ret += `namespace ${it.name} `;
+      ret += `{${iterate(it.members)}};`;
+      return ret;
+    }
+
+    function dictionary(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
+      curPea = "";
+      if (it.partial) ret += "partial ";
+      ret += `dictionary ${it.name} `;
+      if (it.inheritance) ret += `: ${it.inheritance} `;
+      ret += `{${iterate(it.members)}};`;
       return ret;
     };
-    var field = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function field(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
       if (it.required) ret += "required ";
-      ret += type(it.idlType) + " " + it.name;
-      if (it["default"]) ret += " = " + const_value(it["default"]);
+      ret += `${type(it.idlType)} ${it.name}`;
+      if (it["default"]) ret += ` = ${const_value(it["default"])}`;
       ret += ";";
       return ret;
     };
-    var exception = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function const_(it) {
+      const ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
-      ret += "exception " + it.name + " ";
-      if (it.inheritance) ret += ": " + it.inheritance + " ";
-      ret += "{" + iterate(it.members) + "};";
-      return ret;
+      return `${ret}const ${type(it.idlType)}${it.nullable ? "?" : ""} ${it.name} = ${const_value(it.value)};`;
     };
-    var const_ = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function typedef(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
-      return ret + "const " + type(it.idlType) + " " + it.name + " = " + const_value(it.value) + ";";
-    };
-    var typedef = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
-      curPea = "";
-      ret += "typedef " + extended_attributes(it.typeExtAttrs, curTPea);
+      ret += `typedef ${extended_attributes(it.typeExtAttrs, curTPea)}`;
       curTPea = "";
-      return ret + type(it.idlType) + " " + it.name + ";";
+      return `${ret}${type(it.idlType)} ${it.name};`;
     };
-    var implements_ = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function implements_(it) {
+      const ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
-      return ret + it.target + " implements " + it["implements"] + ";";
+      return `${ret}${it.target} implements ${it["implements"]};`;
     };
-    var callback = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function includes(it) {
+      const ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
-      return ret + "callback " + it.name + " = " + type(it.idlType) +
-        "(" + args(it["arguments"]) + ");";
+      return `${ret}${it.target} includes ${it.includes};`;
     };
-    var enum_ = function(it) {
-      var ret = extended_attributes(it.extAttrs, curPea);
+    function callback(it) {
+      const ret = extended_attributes(it.extAttrs, curPea);
       curPea = "";
-      ret += "enum " + it.name + " {";
-      for (var i = 0, n = it.values.length; i < n; i++) {
-        var v = it.values[i];
-        if (typeof v === "string") ret += '"' + v + '"';
+      return `${ret}callback ${it.name} = ${type(it.idlType)}(${args(it["arguments"])});`;
+    };
+    function enum_(it) {
+      let ret = extended_attributes(it.extAttrs, curPea);
+      curPea = "";
+      ret += `enum ${it.name} {`;
+      for (const v of it.values) {
+        if (v.type === "string") ret += `"${v.value}"`;
         else if (v.type === "ws") ret += v.value;
         else if (v.type === ",") ret += ",";
       }
       return ret + "};";
     };
-    var iterable = function(it) {
-      return "iterable<" + (it.idlType instanceof Array ? it.idlType.map(type).join(", ") : type(it.idlType)) + ">;";
+    function iterable(it) {
+      return `iterable<${Array.isArray(it.idlType) ? it.idlType.map(type).join(", ") : type(it.idlType)}>;`;
     };
-    var legacyiterable = function(it) {
-      return "legacyiterable<" + (it.idlType instanceof Array ? it.idlType.map(type).join(", ") : type(it.idlType)) + ">;";
+    function legacyiterable(it) {
+      return `legacyiterable<${Array.isArray(it.idlType) ? it.idlType.map(type).join(", ") : type(it.idlType)}>;`;
     };
-    var maplike = function(it) {
-      return (it.readonly ? "readonly " : "") + "maplike<" +
-        it.idlType.map(type).join(", ") + ">;";
+    function maplike(it) {
+      return `${it.readonly ? "readonly " : ""}maplike<${it.idlType.map(type).join(", ")}>;`;
     };
-    var setlike = function(it) {
-      return (it.readonly ? "readonly " : "") + "setlike<" +
-        type(it.idlType) + ">;";
+    function setlike(it) {
+      return `${it.readonly ? "readonly " : ""}setlike<${type(it.idlType[0])}>;`;
     };
-    var callbackInterface = function(it) {
-      return 'callback ' + interface_(it);
+    function callbackInterface(it) {
+      return `callback ${interface_(it)}`;
     };
 
-    var table = {
+    const table = {
       ws: literal,
       "ws-pea": wsPea,
       "ws-tpea": wsTPea,
       "line-comment": lineComment,
       "multiline-comment": multilineComment,
-      "interface": interface_,
-      operation: operation,
-      attribute: attribute,
-      dictionary: dictionary,
-      field: field,
-      exception: exception,
-      "const": const_,
-      typedef: typedef,
-      "implements": implements_,
-      callback: callback,
-      "enum": enum_,
-      iterable: iterable,
-      legacyiterable: legacyiterable,
-      maplike: maplike,
-      setlike: setlike,
+      interface: interface_,
+      "interface mixin": interface_mixin,
+      namespace,
+      operation,
+      attribute,
+      dictionary,
+      field,
+      const: const_,
+      typedef,
+      implements: implements_,
+      includes,
+      callback,
+      enum: enum_,
+      iterable,
+      legacyiterable,
+      maplike,
+      setlike,
       "callback interface": callbackInterface
     };
-    var dispatch = function(it) {
+    function dispatch(it) {
+      const dispatcher = table[it.type];
+      if (!dispatcher) {
+        throw new Error(`Type "${it.type}" is unsupported`)
+      }
       return table[it.type](it);
     };
-    var iterate = function(things) {
+    function iterate(things) {
       if (!things) return;
-      var ret = "";
-      for (var i = 0, n = things.length; i < n; i++) ret += dispatch(things[i]);
+      let ret = "";
+      for (const thing of things) ret += dispatch(thing);
       return ret;
     };
     return iterate(ast);
   };
 
 
-  var obj = {
-    write: function(ast, opt) {
-      if (!opt) opt = {};
-      return write(ast, opt);
-    }
+  const obj = {
+    write
   };
 
   if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
     module.exports = obj;
   } else if (typeof define === 'function' && define.amd) {
-    define([], function() {
-      return obj;
-    });
+    define([], () => obj);
   } else {
     (self || window).WebIDL2Writer = obj;
   }
-}());
+})();
diff --git a/resources/webidl2/package-lock.json b/resources/webidl2/package-lock.json
index 5352871..a6f529d 100644
--- a/resources/webidl2/package-lock.json
+++ b/resources/webidl2/package-lock.json
@@ -1,25 +1,52 @@
 {
   "name": "webidl2",
-  "version": "4.2.0",
+  "version": "10.2.1",
   "lockfileVersion": 1,
+  "requires": true,
   "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.0.0-beta.40",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz",
+      "integrity": "sha512-eVXQSbu/RimU6OKcK2/gDJVTFcxXJI4sHbIqw2mhwMZeQ2as/8AhS9DGkEDoHMBBNJZ5B0US63lF56x+KDcxiA==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "7.0.0-beta.40"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0-beta.40",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.40.tgz",
+      "integrity": "sha512-mOhhTrzieV6VO7odgzFGFapiwRK0ei8RZRhfzHhb6cpX3QM8XXuCLXWjN8qBB7JReDdUR80V3LFfFrGUYevhNg==",
+      "dev": true,
+      "requires": {
+        "chalk": "2.3.2",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
     "ansi-regex": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz",
-      "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+      "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
       "dev": true
     },
     "ansi-styles": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz",
-      "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=",
-      "dev": true
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "1.9.1"
+      }
     },
     "arr-diff": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
       "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "arr-flatten": "1.1.0"
+      }
     },
     "arr-flatten": {
       "version": "1.1.0",
@@ -40,34 +67,51 @@
       "dev": true
     },
     "brace-expansion": {
-      "version": "1.1.8",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
-      "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
-      "dev": true
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
     },
     "braces": {
       "version": "1.8.5",
       "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
       "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "expand-range": "1.8.2",
+        "preserve": "0.2.0",
+        "repeat-element": "1.1.2"
+      }
     },
     "browser-stdout": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
-      "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
       "dev": true
     },
     "chalk": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
-      "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=",
-      "dev": true
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
+      "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "3.2.1",
+        "escape-string-regexp": "1.0.5",
+        "supports-color": "5.3.0"
+      }
     },
     "color-convert": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz",
-      "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=",
-      "dev": true
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+      "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
     },
     "color-name": {
       "version": "1.1.3",
@@ -76,9 +120,9 @@
       "dev": true
     },
     "commander": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
-      "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+      "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
       "dev": true
     },
     "concat-map": {
@@ -88,15 +132,24 @@
       "dev": true
     },
     "debug": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz",
-      "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=",
-      "dev": true
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+      "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
     },
     "diff": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
-      "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "diff-match-patch": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz",
+      "integrity": "sha1-HMPIOkkNZ/ldkeOfatHy4Ia2MEg=",
       "dev": true
     },
     "escape-string-regexp": {
@@ -105,37 +158,52 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
     "expand-brackets": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
       "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-posix-bracket": "0.1.1"
+      }
     },
     "expand-range": {
       "version": "1.8.2",
       "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
       "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "fill-range": "2.2.3"
+      }
     },
     "expect": {
-      "version": "21.2.1",
-      "resolved": "https://registry.npmjs.org/expect/-/expect-21.2.1.tgz",
-      "integrity": "sha512-orfQQqFRTX0jH7znRIGi8ZMR8kTNpXklTTz8+HGTpmTKZo3Occ6JNB5FXMb8cRuiiC/GyDqsr30zUa66ACYlYw==",
+      "version": "22.4.0",
+      "resolved": "https://registry.npmjs.org/expect/-/expect-22.4.0.tgz",
+      "integrity": "sha512-Fiy862jT3qc70hwIHwwCBNISmaqBrfWKKrtqyMJ6iwZr+6KXtcnHojZFtd63TPRvRl8EQTJ+YXYy2lK6/6u+Hw==",
       "dev": true,
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
-          "dev": true
-        }
+      "requires": {
+        "ansi-styles": "3.2.1",
+        "jest-diff": "22.4.0",
+        "jest-get-type": "22.1.0",
+        "jest-matcher-utils": "22.4.0",
+        "jest-message-util": "22.4.0",
+        "jest-regex-util": "22.1.0"
       }
     },
     "extglob": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
       "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
     },
     "filename-regex": {
       "version": "2.0.1",
@@ -147,7 +215,14 @@
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
       "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-number": "2.1.0",
+        "isobject": "2.1.0",
+        "randomatic": "1.1.7",
+        "repeat-element": "1.1.2",
+        "repeat-string": "1.6.1"
+      }
     },
     "for-in": {
       "version": "1.0.2",
@@ -159,7 +234,10 @@
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
       "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "for-in": "1.0.2"
+      }
     },
     "fs.realpath": {
       "version": "1.0.0",
@@ -168,52 +246,65 @@
       "dev": true
     },
     "glob": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
-      "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
-      "dev": true
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "1.0.0",
+        "inflight": "1.0.6",
+        "inherits": "2.0.3",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
+      }
     },
     "glob-base": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
       "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "glob-parent": "2.0.0",
+        "is-glob": "2.0.1"
+      }
     },
     "glob-parent": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
       "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
-      "dev": true
-    },
-    "graceful-readlink": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
-      "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-glob": "2.0.1"
+      }
     },
     "growl": {
-      "version": "1.9.2",
-      "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
-      "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
-      "dev": true
-    },
-    "has-ansi": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
-      "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=",
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
+      "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
       "dev": true
     },
     "has-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
-      "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
       "dev": true
     },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
+      }
     },
     "inherits": {
       "version": "2.0.3",
@@ -222,9 +313,9 @@
       "dev": true
     },
     "is-buffer": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz",
-      "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=",
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
       "dev": true
     },
     "is-dotfile": {
@@ -237,7 +328,10 @@
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
       "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-primitive": "2.0.0"
+      }
     },
     "is-extendable": {
       "version": "0.1.1",
@@ -255,13 +349,19 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
       "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
     },
     "is-number": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
       "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "kind-of": "3.2.2"
+      }
     },
     "is-posix-bracket": {
       "version": "0.1.1",
@@ -285,199 +385,113 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
       "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "isarray": "1.0.0"
+      }
     },
     "jest-diff": {
-      "version": "21.2.1",
-      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-21.2.1.tgz",
-      "integrity": "sha512-E5fu6r7PvvPr5qAWE1RaUwIh/k6Zx/3OOkZ4rk5dBJkEWRrUuSgbMt2EO8IUTPTd6DOqU3LW6uTIwX5FRvXoFA==",
+      "version": "22.4.0",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.0.tgz",
+      "integrity": "sha512-+/t20WmnkOkB8MOaGaPziI8zWKxquMvYw4Ub+wOzi7AUhmpFXz43buWSxVoZo4J5RnCozpGbX3/FssjJ5KV9Nw==",
       "dev": true,
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
-          "dev": true
-        },
-        "chalk": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
-          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "4.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
-          "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
-          "dev": true
-        }
+      "requires": {
+        "chalk": "2.3.2",
+        "diff": "3.5.0",
+        "jest-get-type": "22.1.0",
+        "pretty-format": "22.4.0"
       }
     },
     "jest-get-type": {
-      "version": "21.2.0",
-      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz",
-      "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==",
+      "version": "22.1.0",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.1.0.tgz",
+      "integrity": "sha512-nD97IVOlNP6fjIN5i7j5XRH+hFsHL7VlauBbzRvueaaUe70uohrkz7pL/N8lx/IAwZRTJ//wOdVgh85OgM7g3w==",
       "dev": true
     },
     "jest-matcher-utils": {
-      "version": "21.2.1",
-      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz",
-      "integrity": "sha512-kn56My+sekD43dwQPrXBl9Zn9tAqwoy25xxe7/iY4u+mG8P3ALj5IK7MLHZ4Mi3xW7uWVCjGY8cm4PqgbsqMCg==",
+      "version": "22.4.0",
+      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.0.tgz",
+      "integrity": "sha512-03m3issxUXpWMwDYTfmL8hRNewUB0yCRTeXPm+eq058rZxLHD9f5NtSSO98CWHqe4UyISIxd9Ao9iDVjHWd2qg==",
       "dev": true,
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
-          "dev": true
-        },
-        "chalk": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
-          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "4.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
-          "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
-          "dev": true
-        }
+      "requires": {
+        "chalk": "2.3.2",
+        "jest-get-type": "22.1.0",
+        "pretty-format": "22.4.0"
       }
     },
     "jest-message-util": {
-      "version": "21.2.1",
-      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-21.2.1.tgz",
-      "integrity": "sha512-EbC1X2n0t9IdeMECJn2BOg7buOGivCvVNjqKMXTzQOu7uIfLml+keUfCALDh8o4rbtndIeyGU8/BKfoTr/LVDQ==",
+      "version": "22.4.0",
+      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.0.tgz",
+      "integrity": "sha512-eyCJB0T3hrlpFF2FqQoIB093OulP+1qvATQmD3IOgJgMGqPL6eYw8TbC5P/VCWPqKhGL51xvjIIhow5eZ2wHFw==",
       "dev": true,
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
-          "dev": true
-        },
-        "chalk": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
-          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "4.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
-          "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
-          "dev": true
-        }
+      "requires": {
+        "@babel/code-frame": "7.0.0-beta.40",
+        "chalk": "2.3.2",
+        "micromatch": "2.3.11",
+        "slash": "1.0.0",
+        "stack-utils": "1.0.1"
       }
     },
     "jest-regex-util": {
-      "version": "21.2.0",
-      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-21.2.0.tgz",
-      "integrity": "sha512-BKQ1F83EQy0d9Jen/mcVX7D+lUt2tthhK/2gDWRgLDJRNOdRgSp1iVqFxP8EN1ARuypvDflRfPzYT8fQnoBQFQ==",
+      "version": "22.1.0",
+      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-22.1.0.tgz",
+      "integrity": "sha512-on0LqVS6Xeh69sw3d1RukVnur+lVOl3zkmb0Q54FHj9wHoq6dbtWqb3TSlnVUyx36hqjJhjgs/QLqs07Bzu72Q==",
       "dev": true
     },
-    "json3": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
-      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
       "dev": true
     },
     "jsondiffpatch": {
-      "version": "0.2.4",
-      "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.2.4.tgz",
-      "integrity": "sha1-1LbFOz/H2htLkcHCrsi5MrdRHVw=",
-      "dev": true
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.3.5.tgz",
+      "integrity": "sha512-v7eaGLDMCHXH+fsIaZhptEUJmS8EJpunq7IM4cc4vIT/kSRAkaZ6ZF4ebiNcyUelL0znbvj6o2B5Gh9v7Og0BQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "2.3.2",
+        "diff-match-patch": "1.0.0"
+      }
     },
     "kind-of": {
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
       "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-      "dev": true
-    },
-    "lodash._baseassign": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
-      "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
-      "dev": true
-    },
-    "lodash._basecopy": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
-      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
-      "dev": true
-    },
-    "lodash._basecreate": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz",
-      "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=",
-      "dev": true
-    },
-    "lodash._getnative": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
-      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
-      "dev": true
-    },
-    "lodash._isiterateecall": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
-      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
-      "dev": true
-    },
-    "lodash.create": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
-      "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
-      "dev": true
-    },
-    "lodash.isarguments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
-      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
-      "dev": true
-    },
-    "lodash.isarray": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
-      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
-      "dev": true
-    },
-    "lodash.keys": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
-      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-buffer": "1.1.6"
+      }
     },
     "micromatch": {
       "version": "2.3.11",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
       "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "arr-diff": "2.0.0",
+        "array-unique": "0.2.1",
+        "braces": "1.8.5",
+        "expand-brackets": "0.1.5",
+        "extglob": "0.3.2",
+        "filename-regex": "2.0.1",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1",
+        "kind-of": "3.2.2",
+        "normalize-path": "2.1.1",
+        "object.omit": "2.0.1",
+        "parse-glob": "3.0.4",
+        "regex-cache": "0.4.4"
+      }
     },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "brace-expansion": "1.1.11"
+      }
     },
     "minimist": {
       "version": "0.0.8",
@@ -489,51 +503,91 @@
       "version": "0.5.1",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
     },
     "mocha": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.1.tgz",
-      "integrity": "sha1-o4ArSqOBk0yss43nDPdxYh2o+a8=",
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.4.tgz",
+      "integrity": "sha512-nMOpAPFosU1B4Ix1jdhx5e3q7XO55ic5a8cgYvW27CequcEY+BabS0kUVL1Cw1V5PuVHZWeNRWFLmEPexo79VA==",
       "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.11.0",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.3",
+        "he": "1.1.1",
+        "mkdirp": "0.5.1",
+        "supports-color": "4.4.0"
+      },
       "dependencies": {
-        "supports-color": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
-          "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
+        "has-flag": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
           "dev": true
+        },
+        "supports-color": {
+          "version": "4.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
+          "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "2.0.0"
+          }
         }
       }
     },
     "ms": {
-      "version": "0.7.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
-      "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
       "dev": true
     },
     "normalize-path": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
       "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "1.1.0"
+      }
     },
     "object.omit": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
       "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "for-own": "0.1.5",
+        "is-extendable": "0.1.1"
+      }
     },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "wrappy": "1.0.2"
+      }
     },
     "parse-glob": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
       "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "glob-base": "0.3.0",
+        "is-dotfile": "1.0.3",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1"
+      }
     },
     "path-is-absolute": {
       "version": "1.0.1",
@@ -548,23 +602,13 @@
       "dev": true
     },
     "pretty-format": {
-      "version": "21.2.1",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz",
-      "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==",
+      "version": "22.4.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.0.tgz",
+      "integrity": "sha512-pvCxP2iODIIk9adXlo4S3GRj0BrJiil68kByAa1PrgG97c1tClh9dLMgp3Z6cHFZrclaABt0UH8PIhwHuFLqYA==",
       "dev": true,
-      "dependencies": {
-        "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
-          "dev": true
-        },
-        "ansi-styles": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
-          "dev": true
-        }
+      "requires": {
+        "ansi-regex": "3.0.0",
+        "ansi-styles": "3.2.1"
       }
     },
     "randomatic": {
@@ -572,18 +616,28 @@
       "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
       "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
       "dev": true,
+      "requires": {
+        "is-number": "3.0.0",
+        "kind-of": "4.0.0"
+      },
       "dependencies": {
         "is-number": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
           "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
           "dev": true,
+          "requires": {
+            "kind-of": "3.2.2"
+          },
           "dependencies": {
             "kind-of": {
               "version": "3.2.2",
               "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
               "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-              "dev": true
+              "dev": true,
+              "requires": {
+                "is-buffer": "1.1.6"
+              }
             }
           }
         },
@@ -591,7 +645,10 @@
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
           "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
-          "dev": true
+          "dev": true,
+          "requires": {
+            "is-buffer": "1.1.6"
+          }
         }
       }
     },
@@ -599,7 +656,10 @@
       "version": "0.4.4",
       "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
       "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "0.1.3"
+      }
     },
     "remove-trailing-separator": {
       "version": "1.1.0",
@@ -625,17 +685,20 @@
       "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
       "dev": true
     },
-    "strip-ansi": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
-      "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=",
+    "stack-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz",
+      "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=",
       "dev": true
     },
     "supports-color": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz",
-      "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=",
-      "dev": true
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
+      "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
+      "dev": true,
+      "requires": {
+        "has-flag": "3.0.0"
+      }
     },
     "wrappy": {
       "version": "1.0.2",
diff --git a/resources/webidl2/package.json b/resources/webidl2/package.json
index d7b8a42..ab282d1 100644
--- a/resources/webidl2/package.json
+++ b/resources/webidl2/package.json
@@ -1,22 +1,24 @@
 {
   "name": "webidl2",
   "description": "A WebIDL Parser",
-  "version": "8.1.0",
+  "version": "10.2.1",
   "contributors": [
     "Robin Berjon <robin@berjon.com> (https://berjon.com)",
     "Marcos Caceres <marcos@marcosc.com> (https://marcosc.com)",
-    "Kagami Sascha Rosylight <saschaplas@outlook.com>"
+    "Kagami Sascha Rosylight <saschaplas@outlook.com>",
+    "Timothy Gu <timothygu99@gmail.com>"
   ],
   "license": "W3C",
   "dependencies": {},
   "devDependencies": {
-    "expect": "21.2.1",
-    "jsondiffpatch": "0.2.5",
-    "mocha": "4.0.1"
+    "expect": "22.4.0",
+    "jsondiffpatch": "0.3.5",
+    "mocha": "5.0.4"
   },
   "scripts": {
-    "test": "mocha"
+    "test": "mocha",
+    "acquire": "node test/util/acquire.js"
   },
-  "repository": "git://github.com/darobin/webidl2.js",
+  "repository": "git://github.com/w3c/webidl2.js",
   "main": "index"
 }
diff --git a/resources/webidl2/test/invalid.js b/resources/webidl2/test/invalid.js
index 548f0ea..19bbf00 100644
--- a/resources/webidl2/test/invalid.js
+++ b/resources/webidl2/test/invalid.js
@@ -4,37 +4,17 @@
 
 "use strict";
 
-const wp = require("../lib/webidl2");
-const expect = require("expect");
-const pth = require("path");
+const { collect } = require("./util/collect");
 const fs = require("fs");
+const expect = require("expect");
 
 describe("Parses all of the invalid IDLs to check that they blow up correctly", () => {
-  const dir = pth.join(__dirname, "invalid/idl");
-  const skip = {};
-  const idls = fs.readdirSync(dir)
-    .filter(it => (/\.w?idl$/).test(it) && !skip[it])
-    .map(it => pth.join(dir, it));
-  const errors = idls.map(it => pth.join(__dirname, "invalid", "json", pth.basename(it).replace(/\.w?idl/, ".json")));
-
-  for (let i = 0, n = idls.length; i < n; i++) {
-    const idl = idls[i];
-    const err = JSON.parse(fs.readFileSync(errors[i], "utf8"));
-
-    it(`should produce the right error for ${idl}`, () => {
-      let error;
-      try {
-        var ast = wp.parse(fs.readFileSync(idl, "utf8"));
-        console.log(JSON.stringify(ast, null, 4));
-      }
-      catch (e) {
-        error = e;
-      }
-      finally {
-        expect(error).toBeTruthy();
-        expect(error.message).toEqual(err.message);
-        expect(error.line).toEqual(err.line);
-      }
+  for (const test of collect("invalid", { expectError: true })) {
+    it(`should produce the right error for ${test.path}`, () => {
+      const err = test.readJSON();
+      expect(test.error).toBeTruthy();
+      expect(test.error.message).toEqual(err.message);
+      expect(test.error.line).toEqual(err.line);
     });
   }
 });
diff --git a/resources/webidl2/test/syntax/idl/iterator.widl b/resources/webidl2/test/invalid/idl/iterator.widl
similarity index 100%
rename from resources/webidl2/test/syntax/idl/iterator.widl
rename to resources/webidl2/test/invalid/idl/iterator.widl
diff --git a/resources/webidl2/test/invalid/idl/no-semicolon-callback.widl b/resources/webidl2/test/invalid/idl/no-semicolon-callback.widl
new file mode 100644
index 0000000..cb20557
--- /dev/null
+++ b/resources/webidl2/test/invalid/idl/no-semicolon-callback.widl
@@ -0,0 +1,7 @@
+callback interface NoSemicolon {
+  attribute boolean noSemiColon;
+}
+
+enum YouNeedOne {
+  "really"
+}
diff --git a/resources/webidl2/test/invalid/idl/no-semicolon.widl b/resources/webidl2/test/invalid/idl/no-semicolon.widl
new file mode 100644
index 0000000..10bc716
--- /dev/null
+++ b/resources/webidl2/test/invalid/idl/no-semicolon.widl
@@ -0,0 +1,7 @@
+partial interface NoSemicolon {
+  attribute boolean noSemiColon;
+}
+
+enum YouNeedOne {
+  "really"
+}
diff --git a/resources/webidl2/test/invalid/idl/stray-slash.widl b/resources/webidl2/test/invalid/idl/stray-slash.widl
new file mode 100644
index 0000000..b673aa9
--- /dev/null
+++ b/resources/webidl2/test/invalid/idl/stray-slash.widl
@@ -0,0 +1,2 @@
+// This is a comment.
+/ This is not.
diff --git a/resources/webidl2/test/invalid/idl/stringconstants.idl b/resources/webidl2/test/invalid/idl/stringconstants.widl
similarity index 100%
rename from resources/webidl2/test/invalid/idl/stringconstants.idl
rename to resources/webidl2/test/invalid/idl/stringconstants.widl
diff --git a/resources/webidl2/test/invalid/json/array.json b/resources/webidl2/test/invalid/json/array.json
index 30b377f..898b2d8 100644
--- a/resources/webidl2/test/invalid/json/array.json
+++ b/resources/webidl2/test/invalid/json/array.json
@@ -1,4 +1,4 @@
 {
-    "message": "No name in attribute",
+    "message": "Got an error during or right after parsing `interface LotteryResults`: No name in attribute",
     "line": 5
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/invalid/json/caller.json b/resources/webidl2/test/invalid/json/caller.json
index 79b8eca..567fa33 100644
--- a/resources/webidl2/test/invalid/json/caller.json
+++ b/resources/webidl2/test/invalid/json/caller.json
@@ -1,4 +1,4 @@
 {
-    "message": "Invalid operation",
+    "message": "Got an error during or right after parsing `interface NumberQuadrupler`: Invalid operation",
     "line": 6
 }
diff --git a/resources/webidl2/test/invalid/json/dict-required-default.json b/resources/webidl2/test/invalid/json/dict-required-default.json
index c5afeca..82b6b2a 100644
--- a/resources/webidl2/test/invalid/json/dict-required-default.json
+++ b/resources/webidl2/test/invalid/json/dict-required-default.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Required member must not have a default"
+    "message":  "Got an error during or right after parsing `dictionary Dict`: Required member must not have a default"
 ,   "line":     4
 }
diff --git a/resources/webidl2/test/invalid/json/duplicate.json b/resources/webidl2/test/invalid/json/duplicate.json
index cef3358..e88a715 100644
--- a/resources/webidl2/test/invalid/json/duplicate.json
+++ b/resources/webidl2/test/invalid/json/duplicate.json
@@ -1,4 +1,4 @@
 {
-    "message": "The name \"Test\" of type \"typedef\" is already seen",
+    "message": "Got an error during or right after parsing `typedef Test`: The name \"Test\" of type \"typedef\" is already seen",
     "line": 3
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/invalid/json/enum.json b/resources/webidl2/test/invalid/json/enum.json
index 1661158..073ff6c 100644
--- a/resources/webidl2/test/invalid/json/enum.json
+++ b/resources/webidl2/test/invalid/json/enum.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Unexpected value in enum"
+    "message":  "Got an error during or right after parsing `enum foo`: Unexpected value in enum"
 ,   "line":     1
 }
diff --git a/resources/webidl2/test/invalid/json/exception.json b/resources/webidl2/test/invalid/json/exception.json
index f52cda1..ad9fac6 100644
--- a/resources/webidl2/test/invalid/json/exception.json
+++ b/resources/webidl2/test/invalid/json/exception.json
@@ -1,4 +1,4 @@
 {
-    "message": "Unrecognised tokens",
+    "message": "Got an error before parsing any named definition: Unrecognised tokens",
     "line": 4
 }
diff --git a/resources/webidl2/test/invalid/json/iterator.json b/resources/webidl2/test/invalid/json/iterator.json
new file mode 100644
index 0000000..e46d653
--- /dev/null
+++ b/resources/webidl2/test/invalid/json/iterator.json
@@ -0,0 +1,4 @@
+{
+    "message": "Got an error during or right after parsing `interface SessionManager`: Invalid operation",
+    "line": 5
+}
diff --git a/resources/webidl2/test/invalid/json/maplike-1type.json b/resources/webidl2/test/invalid/json/maplike-1type.json
index 859a820..75e7a35 100644
--- a/resources/webidl2/test/invalid/json/maplike-1type.json
+++ b/resources/webidl2/test/invalid/json/maplike-1type.json
@@ -1,4 +1,4 @@
 {
-    "message": "Missing second type argument in maplike declaration",
+    "message": "Got an error during or right after parsing `interface MapLikeOneType`: Missing second type argument in maplike declaration",
     "line": 2
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/invalid/json/module.json b/resources/webidl2/test/invalid/json/module.json
index 3b0984d..9c071cd 100644
--- a/resources/webidl2/test/invalid/json/module.json
+++ b/resources/webidl2/test/invalid/json/module.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Unrecognised tokens"
+    "message":  "Got an error before parsing any named definition: Unrecognised tokens"
 ,   "line":     2
 }
diff --git a/resources/webidl2/test/invalid/json/no-semicolon-callback.json b/resources/webidl2/test/invalid/json/no-semicolon-callback.json
new file mode 100644
index 0000000..1db9d14
--- /dev/null
+++ b/resources/webidl2/test/invalid/json/no-semicolon-callback.json
@@ -0,0 +1,4 @@
+{
+    "message": "Got an error during or right after parsing `callback interface NoSemicolon`: Missing semicolon after interface",
+    "line": 5
+}
diff --git a/resources/webidl2/test/invalid/json/no-semicolon.json b/resources/webidl2/test/invalid/json/no-semicolon.json
new file mode 100644
index 0000000..087532a
--- /dev/null
+++ b/resources/webidl2/test/invalid/json/no-semicolon.json
@@ -0,0 +1,4 @@
+{
+    "message": "Got an error during or right after parsing `partial interface NoSemicolon`: Missing semicolon after interface",
+    "line": 5
+}
diff --git a/resources/webidl2/test/invalid/json/nonnullableany.json b/resources/webidl2/test/invalid/json/nonnullableany.json
index cf5229e..8a1f900 100644
--- a/resources/webidl2/test/invalid/json/nonnullableany.json
+++ b/resources/webidl2/test/invalid/json/nonnullableany.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Type any cannot be made nullable"
+    "message":  "Got an error during or right after parsing `interface NonNullable`: Type any cannot be made nullable"
 ,   "line":     2
 }
diff --git a/resources/webidl2/test/invalid/json/nonnullableobjects.json b/resources/webidl2/test/invalid/json/nonnullableobjects.json
index 23cbb3e..52bd8da 100644
--- a/resources/webidl2/test/invalid/json/nonnullableobjects.json
+++ b/resources/webidl2/test/invalid/json/nonnullableobjects.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Can't nullable more than once"
+    "message":  "Got an error during or right after parsing `interface NonNullable`: Can't nullable more than once"
 ,   "line":     4
 }
diff --git a/resources/webidl2/test/invalid/json/promise-with-extended-attribute.json b/resources/webidl2/test/invalid/json/promise-with-extended-attribute.json
index e9623ce..71212d4 100644
--- a/resources/webidl2/test/invalid/json/promise-with-extended-attribute.json
+++ b/resources/webidl2/test/invalid/json/promise-with-extended-attribute.json
@@ -1,4 +1,4 @@
 {
-    "message": "Promise type cannot have extended attribute",
+    "message": "Got an error during or right after parsing `interface Foo`: Promise type cannot have extended attribute",
     "line": 2
 }
diff --git a/resources/webidl2/test/invalid/json/raises.json b/resources/webidl2/test/invalid/json/raises.json
index 8b67afe..3165b87 100644
--- a/resources/webidl2/test/invalid/json/raises.json
+++ b/resources/webidl2/test/invalid/json/raises.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Unterminated attribute"
+    "message":  "Got an error during or right after parsing `interface Person`: Unterminated attribute"
 ,   "line":     5
 }
diff --git a/resources/webidl2/test/invalid/json/readonly-iterable.json b/resources/webidl2/test/invalid/json/readonly-iterable.json
index c6f52a2..1a09264 100644
--- a/resources/webidl2/test/invalid/json/readonly-iterable.json
+++ b/resources/webidl2/test/invalid/json/readonly-iterable.json
@@ -1,4 +1,4 @@
 {
-    "message": "Invalid operation",
+    "message": "Got an error during or right after parsing `interface ReadonlyIterable`: Invalid operation",
     "line": 2
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/invalid/json/record-key-with-extended-attribute.json b/resources/webidl2/test/invalid/json/record-key-with-extended-attribute.json
index 3945825..4002e7f 100644
--- a/resources/webidl2/test/invalid/json/record-key-with-extended-attribute.json
+++ b/resources/webidl2/test/invalid/json/record-key-with-extended-attribute.json
@@ -1,4 +1,4 @@
 {
-    "message": "Record key cannot have extended attribute",
+    "message": "Got an error during or right after parsing `interface Foo`: Record key cannot have extended attribute",
     "line": 2
 }
diff --git a/resources/webidl2/test/invalid/json/record-key.json b/resources/webidl2/test/invalid/json/record-key.json
index 3b929b9..179d645 100644
--- a/resources/webidl2/test/invalid/json/record-key.json
+++ b/resources/webidl2/test/invalid/json/record-key.json
@@ -1,4 +1,4 @@
 {
-    "message": "Record key must be DOMString, USVString, or ByteString",
+    "message": "Got an error during or right after parsing `interface Foo`: Record key must be DOMString, USVString, or ByteString",
     "line": 2
 }
diff --git a/resources/webidl2/test/invalid/json/scopedname.json b/resources/webidl2/test/invalid/json/scopedname.json
index 8e2cd80..4620d2d 100644
--- a/resources/webidl2/test/invalid/json/scopedname.json
+++ b/resources/webidl2/test/invalid/json/scopedname.json
@@ -1,4 +1,4 @@
 {
-    "message":  "No name in typedef"
+    "message":  "Got an error before parsing any named definition: No name in typedef"
 ,   "line":     2
 }
diff --git a/resources/webidl2/test/invalid/json/sequenceAsAttribute.json b/resources/webidl2/test/invalid/json/sequenceAsAttribute.json
index b714f5d..5b4314a 100644
--- a/resources/webidl2/test/invalid/json/sequenceAsAttribute.json
+++ b/resources/webidl2/test/invalid/json/sequenceAsAttribute.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Attributes cannot accept sequence types"
+    "message":  "Got an error during or right after parsing `interface sequenceAsAttribute`: Attributes cannot accept sequence types"
 ,   "line":     2
 }
diff --git a/resources/webidl2/test/invalid/json/setlike-2types.json b/resources/webidl2/test/invalid/json/setlike-2types.json
index c9e49b6..2900e1b 100644
--- a/resources/webidl2/test/invalid/json/setlike-2types.json
+++ b/resources/webidl2/test/invalid/json/setlike-2types.json
@@ -1,4 +1,4 @@
 {
-    "message": "Unterminated setlike declaration",
+    "message": "Got an error during or right after parsing `interface SetLikeTwoTypes`: Unterminated setlike declaration",
     "line": 2
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/invalid/json/setter-creator.json b/resources/webidl2/test/invalid/json/setter-creator.json
index 07b9f1f..25decb3 100644
--- a/resources/webidl2/test/invalid/json/setter-creator.json
+++ b/resources/webidl2/test/invalid/json/setter-creator.json
@@ -1,4 +1,4 @@
 {
-    "message": "Invalid operation",
+    "message": "Got an error during or right after parsing `interface OrderedMap`: Invalid operation",
     "line": 3
 }
diff --git a/resources/webidl2/test/invalid/json/special-omittable.json b/resources/webidl2/test/invalid/json/special-omittable.json
index 7acb088..c20b28e 100644
--- a/resources/webidl2/test/invalid/json/special-omittable.json
+++ b/resources/webidl2/test/invalid/json/special-omittable.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Invalid operation"
+    "message":  "Got an error during or right after parsing `interface Dictionary`: Invalid operation"
 ,   "line":     6
 }
diff --git a/resources/webidl2/test/invalid/json/stray-slash.json b/resources/webidl2/test/invalid/json/stray-slash.json
new file mode 100644
index 0000000..9c071cd
--- /dev/null
+++ b/resources/webidl2/test/invalid/json/stray-slash.json
@@ -0,0 +1,4 @@
+{
+    "message":  "Got an error before parsing any named definition: Unrecognised tokens"
+,   "line":     2
+}
diff --git a/resources/webidl2/test/invalid/json/stringconstants.json b/resources/webidl2/test/invalid/json/stringconstants.json
index d5bf1a8..1eeb31c 100644
--- a/resources/webidl2/test/invalid/json/stringconstants.json
+++ b/resources/webidl2/test/invalid/json/stringconstants.json
@@ -1,4 +1,4 @@
 {
-    "message":  "No value for const"
+    "message":  "Got an error during or right after parsing `interface Util`: No value for const"
 ,   "line":     2
 }
diff --git a/resources/webidl2/test/invalid/json/typedef-nested.json b/resources/webidl2/test/invalid/json/typedef-nested.json
index d7fb918..8c60814 100644
--- a/resources/webidl2/test/invalid/json/typedef-nested.json
+++ b/resources/webidl2/test/invalid/json/typedef-nested.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Invalid operation"
+    "message":  "Got an error during or right after parsing `interface Widget`: Invalid operation"
 ,   "line":     14
-}
\ No newline at end of file
+}
diff --git a/resources/webidl2/test/syntax.js b/resources/webidl2/test/syntax.js
index 0838f35..05d647e 100644
--- a/resources/webidl2/test/syntax.js
+++ b/resources/webidl2/test/syntax.js
@@ -1,34 +1,14 @@
 "use strict";
 
-const wp = require("../lib/webidl2");
+const { collect } = require("./util/collect");
 const expect = require("expect");
-const pth = require("path");
-const fs = require("fs");
-const jdp = require("jsondiffpatch");
 const debug = true;
 
 describe("Parses all of the IDLs to produce the correct ASTs", () => {
-  const dir = pth.join(__dirname, "syntax/idl");
-  const skip = {}; // use if we have a broken test
-  const idls = fs.readdirSync(dir)
-    .filter(it => (/\.widl$/).test(it) && !skip[it])
-    .map(it => pth.join(dir, it));
-  const jsons = idls.map(it => pth.join(__dirname, "syntax/json", pth.basename(it).replace(".widl", ".json")));
-
-  for (let i = 0, n = idls.length; i < n; i++) {
-    const idl = idls[i];
-    const json = jsons[i];
-
-    it(`should produce the same AST for ${idl}`, () => {
+  for (const test of collect("syntax")) {
+    it(`should produce the same AST for ${test.path}`, () => {
       try {
-        const optFile = pth.join(__dirname, "syntax/opt", pth.basename(json));
-        let opt = undefined;
-        if (fs.existsSync(optFile))
-          opt = JSON.parse(fs.readFileSync(optFile, "utf8"));
-        const diff = jdp.diff(JSON.parse(fs.readFileSync(json, "utf8")),
-          wp.parse(fs.readFileSync(idl, "utf8").replace(/\r\n/g, "\n"), opt));
-        if (diff && debug) console.log(JSON.stringify(diff, null, 4));
-        expect(diff).toBe(undefined);
+        expect(test.diff()).toBeFalsy();
       }
       catch (e) {
         console.log(e.toString());
diff --git a/resources/webidl2/test/syntax/idl/extended-attributes.widl b/resources/webidl2/test/syntax/idl/extended-attributes.widl
index a49463b..57d4f97 100644
--- a/resources/webidl2/test/syntax/idl/extended-attributes.widl
+++ b/resources/webidl2/test/syntax/idl/extended-attributes.widl
@@ -20,4 +20,10 @@
   attribute double cx;
   attribute double cy;
   readonly attribute double circumference;
-};
\ No newline at end of file
+};
+
+// Extracted from https://heycam.github.io/webidl/#idl-annotated-types on 2017-12-15
+[Exposed=Window]
+interface I {
+    attribute [XAttr] (long or Node) attrib;
+};
diff --git a/resources/webidl2/test/syntax/idl/typedef-union.idl b/resources/webidl2/test/syntax/idl/typedef-union.widl
similarity index 100%
rename from resources/webidl2/test/syntax/idl/typedef-union.idl
rename to resources/webidl2/test/syntax/idl/typedef-union.widl
diff --git a/resources/webidl2/test/syntax/json/allowany.json b/resources/webidl2/test/syntax/json/allowany.json
index ee898d6..cd5c6e0 100644
--- a/resources/webidl2/test/syntax/json/allowany.json
+++ b/resources/webidl2/test/syntax/json/allowany.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -30,6 +31,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -43,6 +45,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -62,6 +65,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -82,6 +86,7 @@
                             }
                         ],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/attributes.json b/resources/webidl2/test/syntax/json/attributes.json
index 86fe9d5..c90c70d 100644
--- a/resources/webidl2/test/syntax/json/attributes.json
+++ b/resources/webidl2/test/syntax/json/attributes.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -24,4 +25,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/callback.json b/resources/webidl2/test/syntax/json/callback.json
index 5301c40..f31067d 100644
--- a/resources/webidl2/test/syntax/json/callback.json
+++ b/resources/webidl2/test/syntax/json/callback.json
@@ -3,6 +3,7 @@
         "type": "callback",
         "name": "AsyncOperationCallback",
         "idlType": {
+            "type": "return-type",
             "sequence": false,
             "generic": null,
             "nullable": false,
@@ -15,6 +16,7 @@
                 "variadic": false,
                 "extAttrs": [],
                 "idlType": {
+                    "type": "argument-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -39,6 +41,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -52,6 +55,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -71,6 +75,7 @@
         "type": "callback",
         "name": "SortCallback",
         "idlType": {
+            "type": "return-type",
             "sequence": false,
             "generic": null,
             "nullable": false,
@@ -83,6 +88,7 @@
                 "variadic": false,
                 "extAttrs": [],
                 "idlType": {
+                    "type": "argument-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -96,6 +102,7 @@
                 "variadic": false,
                 "extAttrs": [],
                 "idlType": {
+                    "type": "argument-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/constants.json b/resources/webidl2/test/syntax/json/constants.json
index 7522286..4b98751 100644
--- a/resources/webidl2/test/syntax/json/constants.json
+++ b/resources/webidl2/test/syntax/json/constants.json
@@ -7,7 +7,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "boolean",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "boolean"
+                },
                 "name": "DEBUG",
                 "value": {
                     "type": "boolean",
@@ -18,7 +21,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "short",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "short"
+                },
                 "name": "negative",
                 "value": {
                     "type": "number",
@@ -29,7 +35,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "octet",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "octet"
+                },
                 "name": "LF",
                 "value": {
                     "type": "number",
@@ -40,7 +49,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "unsigned long",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "unsigned long"
+                },
                 "name": "BIT_MASK",
                 "value": {
                     "type": "number",
@@ -51,7 +63,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "float",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "float"
+                },
                 "name": "AVOGADRO",
                 "value": {
                     "type": "number",
@@ -62,7 +77,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "unrestricted float",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "unrestricted float"
+                },
                 "name": "sobig",
                 "value": {
                     "type": "Infinity",
@@ -73,7 +91,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "unrestricted double",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "unrestricted double"
+                },
                 "name": "minusonedividedbyzero",
                 "value": {
                     "type": "Infinity",
@@ -84,7 +105,10 @@
             {
                 "type": "const",
                 "nullable": false,
-                "idlType": "short",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "short"
+                },
                 "name": "notanumber",
                 "value": {
                     "type": "NaN"
@@ -95,4 +119,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/constructor.json b/resources/webidl2/test/syntax/json/constructor.json
index ee55ef0..292236f 100644
--- a/resources/webidl2/test/syntax/json/constructor.json
+++ b/resources/webidl2/test/syntax/json/constructor.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -43,6 +45,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -59,6 +62,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -85,6 +89,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/dictionary-inherits.json b/resources/webidl2/test/syntax/json/dictionary-inherits.json
index ef30b1a..9b928f4 100644
--- a/resources/webidl2/test/syntax/json/dictionary-inherits.json
+++ b/resources/webidl2/test/syntax/json/dictionary-inherits.json
@@ -9,6 +9,7 @@
                 "name": "fillPattern",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -26,6 +27,7 @@
                 "name": "strokePattern",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -42,6 +44,7 @@
                 "name": "position",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -64,6 +67,7 @@
                 "name": "hydrometry",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -76,4 +80,4 @@
         "inheritance": "PaintOptions",
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/dictionary.json b/resources/webidl2/test/syntax/json/dictionary.json
index 28494ce..f74fedc 100644
--- a/resources/webidl2/test/syntax/json/dictionary.json
+++ b/resources/webidl2/test/syntax/json/dictionary.json
@@ -9,6 +9,7 @@
                 "name": "fillPattern",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -26,6 +27,7 @@
                 "name": "strokePattern",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -42,6 +44,7 @@
                 "name": "position",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -55,11 +58,13 @@
                 "name": "seq",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": true,
                     "generic": "sequence",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "dictionary-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -78,6 +83,7 @@
                 "name": "reqSeq",
                 "required": true,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -100,6 +106,7 @@
                 "name": "h",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -113,6 +120,7 @@
                 "name": "d",
                 "required": false,
                 "idlType": {
+                    "type": "dictionary-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -124,4 +132,4 @@
         ],
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/documentation-dos.json b/resources/webidl2/test/syntax/json/documentation-dos.json
index 340e039..baa0b5a 100644
--- a/resources/webidl2/test/syntax/json/documentation-dos.json
+++ b/resources/webidl2/test/syntax/json/documentation-dos.json
@@ -7,4 +7,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/documentation.json b/resources/webidl2/test/syntax/json/documentation.json
index 340e039..baa0b5a 100644
--- a/resources/webidl2/test/syntax/json/documentation.json
+++ b/resources/webidl2/test/syntax/json/documentation.json
@@ -7,4 +7,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/enum.json b/resources/webidl2/test/syntax/json/enum.json
index 88a608b..29d1c86 100644
--- a/resources/webidl2/test/syntax/json/enum.json
+++ b/resources/webidl2/test/syntax/json/enum.json
@@ -3,9 +3,18 @@
         "type": "enum",
         "name": "MealType",
         "values": [
-            { "type": "string", "value": "rice" },
-            { "type": "string", "value": "noodles" },
-            { "type": "string", "value": "other" }
+            {
+                "type": "string",
+                "value": "rice"
+            },
+            {
+                "type": "string",
+                "value": "noodles"
+            },
+            {
+                "type": "string",
+                "value": "other"
+            }
         ],
         "extAttrs": []
     },
@@ -21,6 +30,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -37,6 +47,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -54,6 +65,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -67,6 +79,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -80,6 +93,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -99,9 +113,18 @@
         "type": "enum",
         "name": "AltMealType",
         "values": [
-            { "type": "string", "value": "rice" },
-            { "type": "string", "value": "noodles" },
-            { "type": "string", "value": "other" }
+            {
+                "type": "string",
+                "value": "rice"
+            },
+            {
+                "type": "string",
+                "value": "noodles"
+            },
+            {
+                "type": "string",
+                "value": "other"
+            }
         ],
         "extAttrs": []
     }
diff --git a/resources/webidl2/test/syntax/json/equivalent-decl.json b/resources/webidl2/test/syntax/json/equivalent-decl.json
index 7ef08bc..de8d4db 100644
--- a/resources/webidl2/test/syntax/json/equivalent-decl.json
+++ b/resources/webidl2/test/syntax/json/equivalent-decl.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -60,6 +63,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +77,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -86,6 +91,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -113,6 +119,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -130,6 +137,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -143,6 +151,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -162,6 +171,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -175,6 +185,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -188,6 +199,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -207,6 +219,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -220,6 +233,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -239,6 +253,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -252,6 +267,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -265,6 +281,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/extended-attributes.json b/resources/webidl2/test/syntax/json/extended-attributes.json
index 6526e2a..e0dc236 100644
--- a/resources/webidl2/test/syntax/json/extended-attributes.json
+++ b/resources/webidl2/test/syntax/json/extended-attributes.json
@@ -30,139 +30,205 @@
         ]
     },
     {
-      "type": "interface",
-      "name": "IdInterface",
-      "partial": false,
-      "members": [],
-      "inheritance": null,
-      "extAttrs": [
-        {
-          "name": "IntAttr",
-          "arguments": null,
-          "type": "extended-attribute",
-          "rhs": {
-            "type": "integer",
-            "value": "0"
-          }
-        },
-        {
-          "name": "FloatAttr",
-          "arguments": null,
-          "type": "extended-attribute",
-          "rhs": {
-            "type": "float",
-            "value": "3.14"
-          }
-        },
-        {
-          "name": "StringAttr",
-          "arguments": null,
-          "type": "extended-attribute",
-          "rhs": {
-            "type": "string",
-            "value": "\"abc\""
-          }
-        }
-      ]
+        "type": "interface",
+        "name": "IdInterface",
+        "partial": false,
+        "members": [],
+        "inheritance": null,
+        "extAttrs": [
+            {
+                "name": "IntAttr",
+                "arguments": null,
+                "type": "extended-attribute",
+                "rhs": {
+                    "type": "integer",
+                    "value": "0"
+                }
+            },
+            {
+                "name": "FloatAttr",
+                "arguments": null,
+                "type": "extended-attribute",
+                "rhs": {
+                    "type": "float",
+                    "value": "3.14"
+                }
+            },
+            {
+                "name": "StringAttr",
+                "arguments": null,
+                "type": "extended-attribute",
+                "rhs": {
+                    "type": "string",
+                    "value": "\"abc\""
+                }
+            }
+        ]
     },
     {
-      "type": "interface",
-      "name": "Circle",
-      "partial": false,
-      "members": [
-        {
-          "type": "attribute",
-          "static": false,
-          "stringifier": false,
-          "inherit": false,
-          "readonly": false,
-          "idlType": {
-            "sequence": false,
-            "generic": null,
-            "nullable": false,
-            "union": false,
-            "idlType": "double"
-          },
-          "name": "r",
-          "extAttrs": []
-        },
-        {
-          "type": "attribute",
-          "static": false,
-          "stringifier": false,
-          "inherit": false,
-          "readonly": false,
-          "idlType": {
-            "sequence": false,
-            "generic": null,
-            "nullable": false,
-            "union": false,
-            "idlType": "double"
-          },
-          "name": "cx",
-          "extAttrs": []
-        },
-        {
-          "type": "attribute",
-          "static": false,
-          "stringifier": false,
-          "inherit": false,
-          "readonly": false,
-          "idlType": {
-            "sequence": false,
-            "generic": null,
-            "nullable": false,
-            "union": false,
-            "idlType": "double"
-          },
-          "name": "cy",
-          "extAttrs": []
-        },
-        {
-          "type": "attribute",
-          "static": false,
-          "stringifier": false,
-          "inherit": false,
-          "readonly": true,
-          "idlType": {
-            "sequence": false,
-            "generic": null,
-            "nullable": false,
-            "union": false,
-            "idlType": "double"
-          },
-          "name": "circumference",
-          "extAttrs": []
-        }
-      ],
-      "inheritance": null,
-      "extAttrs": [
-        {
-          "name": "Constructor",
-          "arguments": null,
-          "type": "extended-attribute",
-          "rhs": null
-        },
-        {
-          "name": "Constructor",
-          "arguments": [
+        "type": "interface",
+        "name": "Circle",
+        "partial": false,
+        "members": [
             {
-              "optional": false,
-              "variadic": false,
-              "extAttrs": [],
-              "idlType": {
-                "sequence": false,
-                "generic": null,
-                "nullable": false,
-                "union": false,
-                "idlType": "double"
-              },
-              "name": "radius"
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "double"
+                },
+                "name": "r",
+                "extAttrs": []
+            },
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "double"
+                },
+                "name": "cx",
+                "extAttrs": []
+            },
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "double"
+                },
+                "name": "cy",
+                "extAttrs": []
+            },
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": true,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "double"
+                },
+                "name": "circumference",
+                "extAttrs": []
             }
-          ],
-          "type": "extended-attribute",
-          "rhs": null
-        }
-      ]
+        ],
+        "inheritance": null,
+        "extAttrs": [
+            {
+                "name": "Constructor",
+                "arguments": null,
+                "type": "extended-attribute",
+                "rhs": null
+            },
+            {
+                "name": "Constructor",
+                "arguments": [
+                    {
+                        "optional": false,
+                        "variadic": false,
+                        "extAttrs": [],
+                        "idlType": {
+                            "type": "argument-type",
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "double"
+                        },
+                        "name": "radius"
+                    }
+                ],
+                "type": "extended-attribute",
+                "rhs": null
+            }
+        ]
+    },
+    {
+        "type": "interface",
+        "name": "I",
+        "partial": false,
+        "members": [
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": true,
+                    "idlType": [
+                        {
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "long"
+                        },
+                        {
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "Node"
+                        }
+                    ],
+                    "extAttrs": [
+                        {
+                            "name": "XAttr",
+                            "arguments": null,
+                            "type": "extended-attribute",
+                            "rhs": null
+                        }
+                    ]
+                },
+                "name": "attrib",
+                "extAttrs": []
+            }
+        ],
+        "inheritance": null,
+        "extAttrs": [
+            {
+                "name": "Exposed",
+                "arguments": null,
+                "type": "extended-attribute",
+                "rhs": {
+                    "type": "identifier",
+                    "value": "Window"
+                }
+            }
+        ]
     }
 ]
diff --git a/resources/webidl2/test/syntax/json/generic.json b/resources/webidl2/test/syntax/json/generic.json
index 214ddd8..6259385 100644
--- a/resources/webidl2/test/syntax/json/generic.json
+++ b/resources/webidl2/test/syntax/json/generic.json
@@ -12,21 +12,25 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "Promise",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "return-type",
                         "sequence": false,
                         "generic": "ResponsePromise",
                         "nullable": false,
                         "union": false,
                         "idlType": {
+                            "type": "return-type",
                             "sequence": true,
                             "generic": "sequence",
                             "nullable": false,
                             "union": false,
                             "idlType": {
+                                "type": "return-type",
                                 "sequence": false,
                                 "generic": null,
                                 "nullable": true,
@@ -47,11 +51,13 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": "Promise",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "attribute-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -79,11 +85,13 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "Promise",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "return-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": true,
@@ -103,11 +111,13 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "Promise",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "return-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -136,11 +146,13 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "ResponsePromise",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "return-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/getter-setter.json b/resources/webidl2/test/syntax/json/getter-setter.json
index 6268a53..1213307 100644
--- a/resources/webidl2/test/syntax/json/getter-setter.json
+++ b/resources/webidl2/test/syntax/json/getter-setter.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -60,6 +63,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +77,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -86,6 +91,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/identifier-qualified-names.json b/resources/webidl2/test/syntax/json/identifier-qualified-names.json
index 11568f8..f4e295a 100644
--- a/resources/webidl2/test/syntax/json/identifier-qualified-names.json
+++ b/resources/webidl2/test/syntax/json/identifier-qualified-names.json
@@ -2,6 +2,7 @@
     {
         "type": "typedef",
         "idlType": {
+            "type": "typedef-type",
             "sequence": false,
             "generic": null,
             "nullable": false,
@@ -24,6 +25,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -37,6 +39,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -56,6 +59,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -69,6 +73,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -96,6 +101,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -112,6 +118,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -138,6 +145,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -151,6 +159,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/implements.json b/resources/webidl2/test/syntax/json/implements.json
index fba0542..69879d9 100644
--- a/resources/webidl2/test/syntax/json/implements.json
+++ b/resources/webidl2/test/syntax/json/implements.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -37,6 +38,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -50,6 +52,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -63,6 +66,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -76,6 +80,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/indexed-properties.json b/resources/webidl2/test/syntax/json/indexed-properties.json
index 2676c0e..697b595 100644
--- a/resources/webidl2/test/syntax/json/indexed-properties.json
+++ b/resources/webidl2/test/syntax/json/indexed-properties.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -60,6 +63,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +77,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -86,6 +91,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -105,6 +111,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -118,6 +125,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -137,6 +145,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -150,6 +159,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -169,6 +179,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -182,6 +193,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -195,6 +207,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -214,6 +227,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -227,6 +241,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/inherits-getter.json b/resources/webidl2/test/syntax/json/inherits-getter.json
index ba6bafd..818d8b6 100644
--- a/resources/webidl2/test/syntax/json/inherits-getter.json
+++ b/resources/webidl2/test/syntax/json/inherits-getter.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -36,6 +37,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -52,6 +54,7 @@
                 "inherit": true,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -65,4 +68,4 @@
         "inheritance": "Animal",
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/interface-inherits.json b/resources/webidl2/test/syntax/json/interface-inherits.json
index e78c1cc..74a4c39 100644
--- a/resources/webidl2/test/syntax/json/interface-inherits.json
+++ b/resources/webidl2/test/syntax/json/interface-inherits.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -36,6 +37,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -61,6 +63,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -74,4 +77,4 @@
         "inheritance": "Animal",
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/iterable.json b/resources/webidl2/test/syntax/json/iterable.json
index 1e92307..7126a4e 100644
--- a/resources/webidl2/test/syntax/json/iterable.json
+++ b/resources/webidl2/test/syntax/json/iterable.json
@@ -6,13 +6,16 @@
         "members": [
             {
                 "type": "iterable",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long"
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long"
+                    }
+                ],
                 "extAttrs": []
             }
         ],
@@ -28,6 +31,7 @@
                 "type": "iterable",
                 "idlType": [
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -35,6 +39,7 @@
                         "idlType": "short"
                     },
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": true,
@@ -55,21 +60,24 @@
         "members": [
             {
                 "type": "iterable",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long",
-                    "extAttrs": [
-                        {
-                            "name": "XAttr",
-                            "arguments": null,
-                            "type": "extended-attribute",
-                            "rhs": null
-                        }
-                    ]
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long",
+                        "extAttrs": [
+                            {
+                                "name": "XAttr",
+                                "arguments": null,
+                                "type": "extended-attribute",
+                                "rhs": null
+                            }
+                        ]
+                    }
+                ],
                 "extAttrs": []
             }
         ],
@@ -77,4 +85,3 @@
         "extAttrs": []
     }
 ]
-
diff --git a/resources/webidl2/test/syntax/json/legacyiterable.json b/resources/webidl2/test/syntax/json/legacyiterable.json
index 059ccc1..5a1e526 100644
--- a/resources/webidl2/test/syntax/json/legacyiterable.json
+++ b/resources/webidl2/test/syntax/json/legacyiterable.json
@@ -6,13 +6,16 @@
         "members": [
             {
                 "type": "legacyiterable",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long"
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long"
+                    }
+                ],
                 "extAttrs": []
             }
         ],
@@ -20,4 +23,3 @@
         "extAttrs": []
     }
 ]
-
diff --git a/resources/webidl2/test/syntax/json/maplike.json b/resources/webidl2/test/syntax/json/maplike.json
index 97e76d5..b86e104 100644
--- a/resources/webidl2/test/syntax/json/maplike.json
+++ b/resources/webidl2/test/syntax/json/maplike.json
@@ -8,6 +8,7 @@
                 "type": "maplike",
                 "idlType": [
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -15,6 +16,7 @@
                         "idlType": "long"
                     },
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -38,6 +40,7 @@
                 "type": "maplike",
                 "idlType": [
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -45,6 +48,7 @@
                         "idlType": "long"
                     },
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -68,6 +72,7 @@
                 "type": "maplike",
                 "idlType": [
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -83,6 +88,7 @@
                         ]
                     },
                     {
+                        "type": null,
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -106,4 +112,3 @@
         "extAttrs": []
     }
 ]
-
diff --git a/resources/webidl2/test/syntax/json/mixin.json b/resources/webidl2/test/syntax/json/mixin.json
index a2afae3..f0458e2 100644
--- a/resources/webidl2/test/syntax/json/mixin.json
+++ b/resources/webidl2/test/syntax/json/mixin.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -47,6 +48,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/namedconstructor.json b/resources/webidl2/test/syntax/json/namedconstructor.json
index cee1630..f895461 100644
--- a/resources/webidl2/test/syntax/json/namedconstructor.json
+++ b/resources/webidl2/test/syntax/json/namedconstructor.json
@@ -23,6 +23,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -40,4 +41,4 @@
             }
         ]
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/namespace.json b/resources/webidl2/test/syntax/json/namespace.json
index 8ee7796..0611b71 100644
--- a/resources/webidl2/test/syntax/json/namespace.json
+++ b/resources/webidl2/test/syntax/json/namespace.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -54,6 +57,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -73,6 +77,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -86,6 +91,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -99,6 +105,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/nointerfaceobject.json b/resources/webidl2/test/syntax/json/nointerfaceobject.json
index 85987e8..03bb994 100644
--- a/resources/webidl2/test/syntax/json/nointerfaceobject.json
+++ b/resources/webidl2/test/syntax/json/nointerfaceobject.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -25,6 +26,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/nullable.json b/resources/webidl2/test/syntax/json/nullable.json
index 2c83c3d..ae4d2aa 100644
--- a/resources/webidl2/test/syntax/json/nullable.json
+++ b/resources/webidl2/test/syntax/json/nullable.json
@@ -7,7 +7,10 @@
             {
                 "type": "const",
                 "nullable": true,
-                "idlType": "boolean",
+                "idlType": {
+                    "type": "const-type",
+                    "idlType": "boolean"
+                },
                 "name": "ARE_WE_THERE_YET",
                 "value": {
                     "type": "boolean",
@@ -31,6 +34,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -44,4 +48,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/nullableobjects.json b/resources/webidl2/test/syntax/json/nullableobjects.json
index 2c87560..29d1314 100644
--- a/resources/webidl2/test/syntax/json/nullableobjects.json
+++ b/resources/webidl2/test/syntax/json/nullableobjects.json
@@ -28,6 +28,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +42,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": true,
@@ -60,6 +62,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +76,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": true,
diff --git a/resources/webidl2/test/syntax/json/operation-optional-arg.json b/resources/webidl2/test/syntax/json/operation-optional-arg.json
index 7588d63..44c3a16 100644
--- a/resources/webidl2/test/syntax/json/operation-optional-arg.json
+++ b/resources/webidl2/test/syntax/json/operation-optional-arg.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -25,6 +26,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -38,6 +40,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -51,6 +54,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -64,6 +68,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/overloading.json b/resources/webidl2/test/syntax/json/overloading.json
index 8d7e8e2..87169e9 100644
--- a/resources/webidl2/test/syntax/json/overloading.json
+++ b/resources/webidl2/test/syntax/json/overloading.json
@@ -28,6 +28,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +42,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -60,6 +62,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +76,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -101,6 +105,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -114,6 +119,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -133,6 +139,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -153,6 +160,7 @@
                             }
                         ],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -166,6 +174,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -179,6 +188,7 @@
                         "variadic": true,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -198,6 +208,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -216,6 +227,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -229,6 +241,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -242,6 +255,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -255,6 +269,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -268,6 +283,7 @@
                         "variadic": true,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/overridebuiltins.json b/resources/webidl2/test/syntax/json/overridebuiltins.json
index 4ded17c..d63f8f7 100644
--- a/resources/webidl2/test/syntax/json/overridebuiltins.json
+++ b/resources/webidl2/test/syntax/json/overridebuiltins.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/partial-interface.json b/resources/webidl2/test/syntax/json/partial-interface.json
index e95af2b..c16c64d 100644
--- a/resources/webidl2/test/syntax/json/partial-interface.json
+++ b/resources/webidl2/test/syntax/json/partial-interface.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -36,6 +37,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -48,4 +50,4 @@
         ],
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/primitives.json b/resources/webidl2/test/syntax/json/primitives.json
index 7568542..cf96539 100644
--- a/resources/webidl2/test/syntax/json/primitives.json
+++ b/resources/webidl2/test/syntax/json/primitives.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -43,6 +45,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -59,6 +62,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -75,6 +79,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -91,6 +96,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -107,6 +113,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -123,6 +130,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -139,6 +147,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -155,6 +164,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -171,6 +181,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -187,6 +198,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -203,6 +215,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -219,6 +232,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -235,6 +249,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -251,6 +266,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -267,6 +283,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -280,4 +297,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/prototyperoot.json b/resources/webidl2/test/syntax/json/prototyperoot.json
index c75f00e..eda7f14 100644
--- a/resources/webidl2/test/syntax/json/prototyperoot.json
+++ b/resources/webidl2/test/syntax/json/prototyperoot.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/putforwards.json b/resources/webidl2/test/syntax/json/putforwards.json
index 1b0a7b6..4b809d2 100644
--- a/resources/webidl2/test/syntax/json/putforwards.json
+++ b/resources/webidl2/test/syntax/json/putforwards.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -37,6 +38,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -50,4 +52,4 @@
         "inheritance": null,
         "extAttrs": []
     }
-]
\ No newline at end of file
+]
diff --git a/resources/webidl2/test/syntax/json/record.json b/resources/webidl2/test/syntax/json/record.json
index 339ae08..dd00b5d 100644
--- a/resources/webidl2/test/syntax/json/record.json
+++ b/resources/webidl2/test/syntax/json/record.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -25,17 +26,20 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": true,
                             "generic": "sequence",
                             "nullable": false,
                             "union": false,
                             "idlType": {
+                                "type": "argument-type",
                                 "sequence": false,
                                 "generic": "record",
                                 "nullable": false,
                                 "union": false,
                                 "idlType": [
                                     {
+                                        "type": "argument-type",
                                         "sequence": false,
                                         "generic": null,
                                         "nullable": false,
@@ -43,6 +47,7 @@
                                         "idlType": "ByteString"
                                     },
                                     {
+                                        "type": "argument-type",
                                         "sequence": false,
                                         "generic": null,
                                         "nullable": false,
@@ -65,12 +70,14 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "record",
                     "nullable": false,
                     "union": false,
                     "idlType": [
                         {
+                            "type": "return-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -78,12 +85,14 @@
                             "idlType": "DOMString"
                         },
                         {
+                            "type": "return-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": true,
                             "union": true,
                             "idlType": [
                                 {
+                                    "type": null,
                                     "sequence": false,
                                     "generic": null,
                                     "nullable": false,
@@ -91,6 +100,7 @@
                                     "idlType": "float"
                                 },
                                 {
+                                    "type": null,
                                     "sequence": false,
                                     "generic": null,
                                     "nullable": false,
@@ -113,6 +123,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -134,12 +145,14 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": "record",
                             "nullable": false,
                             "union": false,
                             "idlType": [
                                 {
+                                    "type": "argument-type",
                                     "sequence": false,
                                     "generic": null,
                                     "nullable": false,
@@ -147,6 +160,7 @@
                                     "idlType": "USVString"
                                 },
                                 {
+                                    "type": "argument-type",
                                     "sequence": false,
                                     "generic": null,
                                     "nullable": false,
@@ -176,12 +190,14 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": "record",
                     "nullable": false,
                     "union": false,
                     "idlType": [
                         {
+                            "type": "return-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -189,6 +205,7 @@
                             "idlType": "DOMString"
                         },
                         {
+                            "type": "return-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/reg-operations.json b/resources/webidl2/test/syntax/json/reg-operations.json
index c1c13c7..d696e90 100644
--- a/resources/webidl2/test/syntax/json/reg-operations.json
+++ b/resources/webidl2/test/syntax/json/reg-operations.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -53,6 +55,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -71,6 +74,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -84,6 +88,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -103,6 +108,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -116,6 +122,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -129,6 +136,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/replaceable.json b/resources/webidl2/test/syntax/json/replaceable.json
index f53009b..a10b0bf 100644
--- a/resources/webidl2/test/syntax/json/replaceable.json
+++ b/resources/webidl2/test/syntax/json/replaceable.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -35,6 +36,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/sequence.json b/resources/webidl2/test/syntax/json/sequence.json
index 7982118..c2d1765 100644
--- a/resources/webidl2/test/syntax/json/sequence.json
+++ b/resources/webidl2/test/syntax/json/sequence.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -25,11 +26,13 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": true,
                             "generic": "sequence",
                             "nullable": false,
                             "union": false,
                             "idlType": {
+                                "type": "argument-type",
                                 "sequence": false,
                                 "generic": null,
                                 "nullable": false,
@@ -50,11 +53,13 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": true,
                     "generic": "sequence",
                     "nullable": false,
                     "union": false,
                     "idlType": {
+                        "type": "return-type",
                         "sequence": false,
                         "generic": null,
                         "nullable": false,
@@ -75,7 +80,6 @@
         "name": "Foo",
         "partial": false,
         "members": [
-
             {
                 "type": "operation",
                 "getter": false,
@@ -84,6 +88,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -98,7 +103,6 @@
         "inheritance": null,
         "extAttrs": []
     },
-
     {
         "type": "interface",
         "name": "I",
@@ -112,6 +116,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -125,11 +130,13 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": true,
                             "generic": "sequence",
                             "nullable": false,
                             "union": false,
                             "idlType": {
+                                "type": "argument-type",
                                 "sequence": false,
                                 "generic": null,
                                 "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/setlike.json b/resources/webidl2/test/syntax/json/setlike.json
index 3c1591b..12299a7 100644
--- a/resources/webidl2/test/syntax/json/setlike.json
+++ b/resources/webidl2/test/syntax/json/setlike.json
@@ -6,13 +6,16 @@
         "members": [
             {
                 "type": "setlike",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long"
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long"
+                    }
+                ],
                 "readonly": false,
                 "extAttrs": []
             }
@@ -27,13 +30,16 @@
         "members": [
             {
                 "type": "setlike",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long"
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long"
+                    }
+                ],
                 "readonly": true,
                 "extAttrs": []
             }
@@ -48,21 +54,24 @@
         "members": [
             {
                 "type": "setlike",
-                "idlType": {
-                    "sequence": false,
-                    "generic": null,
-                    "nullable": false,
-                    "union": false,
-                    "idlType": "long",
-                    "extAttrs": [
-                        {
-                            "name": "XAttr",
-                            "arguments": null,
-                            "type": "extended-attribute",
-                            "rhs": null
-                        }
-                    ]
-                },
+                "idlType": [
+                    {
+                        "type": null,
+                        "sequence": false,
+                        "generic": null,
+                        "nullable": false,
+                        "union": false,
+                        "idlType": "long",
+                        "extAttrs": [
+                            {
+                                "name": "XAttr",
+                                "arguments": null,
+                                "type": "extended-attribute",
+                                "rhs": null
+                            }
+                        ]
+                    }
+                ],
                 "readonly": false,
                 "extAttrs": []
             }
@@ -71,4 +80,3 @@
         "extAttrs": []
     }
 ]
-
diff --git a/resources/webidl2/test/syntax/json/static.json b/resources/webidl2/test/syntax/json/static.json
index 034ffda..0951b2a 100644
--- a/resources/webidl2/test/syntax/json/static.json
+++ b/resources/webidl2/test/syntax/json/static.json
@@ -19,6 +19,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -35,6 +36,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -51,6 +53,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -67,6 +70,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -84,6 +88,7 @@
                 "static": true,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -97,6 +102,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -110,6 +116,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -123,6 +130,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/stringifier-attribute.json b/resources/webidl2/test/syntax/json/stringifier-attribute.json
index acb26c2..36e2b6d 100644
--- a/resources/webidl2/test/syntax/json/stringifier-attribute.json
+++ b/resources/webidl2/test/syntax/json/stringifier-attribute.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/stringifier-custom.json b/resources/webidl2/test/syntax/json/stringifier-custom.json
index 3c84305..3dc3ac1 100644
--- a/resources/webidl2/test/syntax/json/stringifier-custom.json
+++ b/resources/webidl2/test/syntax/json/stringifier-custom.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": true,
@@ -43,6 +45,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -60,6 +63,7 @@
                 "static": false,
                 "stringifier": true,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/stringifier.json b/resources/webidl2/test/syntax/json/stringifier.json
index 6b2d133..1a70276 100644
--- a/resources/webidl2/test/syntax/json/stringifier.json
+++ b/resources/webidl2/test/syntax/json/stringifier.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": true,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/treatasnull.json b/resources/webidl2/test/syntax/json/treatasnull.json
index 03dec60..611d974 100644
--- a/resources/webidl2/test/syntax/json/treatasnull.json
+++ b/resources/webidl2/test/syntax/json/treatasnull.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -44,6 +46,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -67,6 +70,7 @@
                             }
                         ],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/treatasundefined.json b/resources/webidl2/test/syntax/json/treatasundefined.json
index 8699b73..258acda 100644
--- a/resources/webidl2/test/syntax/json/treatasundefined.json
+++ b/resources/webidl2/test/syntax/json/treatasundefined.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -44,6 +46,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -67,6 +70,7 @@
                             }
                         ],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/typedef-union.json b/resources/webidl2/test/syntax/json/typedef-union.json
index 9c87672..06735a8 100644
--- a/resources/webidl2/test/syntax/json/typedef-union.json
+++ b/resources/webidl2/test/syntax/json/typedef-union.json
@@ -1,49 +1,48 @@
 [
-   {
-      "type" : "typedef",
-      "idlType" : {
-         "nullable" : false,
-         "generic" : null,
-         "union" : true,
-         "idlType" : [
-            {
-               "union" : false,
-               "generic" : null,
-               "nullable" : false,
-               "array" : false,
-               "sequence" : false,
-               "idlType" : "ImageData"
-            },
-            {
-               "generic" : null,
-               "union" : false,
-               "nullable" : false,
-               "array" : false,
-               "idlType" : "HTMLImageElement",
-               "sequence" : false
-            },
-            {
-               "array" : false,
-               "sequence" : false,
-               "idlType" : "HTMLCanvasElement",
-               "generic" : null,
-               "union" : false,
-               "nullable" : false
-            },
-            {
-               "union" : false,
-               "generic" : null,
-               "nullable" : false,
-               "array" : false,
-               "sequence" : false,
-               "idlType" : "HTMLVideoElement"
-            }
-         ],
-         "sequence" : false,
-         "array" : false
-      },
-      "name" : "TexImageSource",
-      "extAttrs" : [],
-      "typeExtAttrs" : []
-   }
+    {
+        "type": "typedef",
+        "idlType": {
+            "type": "typedef-type",
+            "sequence": false,
+            "generic": null,
+            "nullable": false,
+            "union": true,
+            "idlType": [
+                {
+                    "type": null,
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "ImageData"
+                },
+                {
+                    "type": null,
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "HTMLImageElement"
+                },
+                {
+                    "type": null,
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "HTMLCanvasElement"
+                },
+                {
+                    "type": null,
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "HTMLVideoElement"
+                }
+            ]
+        },
+        "name": "TexImageSource",
+        "extAttrs": []
+    }
 ]
diff --git a/resources/webidl2/test/syntax/json/typedef.json b/resources/webidl2/test/syntax/json/typedef.json
index 612cfe7..5e9715d 100644
--- a/resources/webidl2/test/syntax/json/typedef.json
+++ b/resources/webidl2/test/syntax/json/typedef.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -27,6 +28,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -43,11 +45,13 @@
     {
         "type": "typedef",
         "idlType": {
+            "type": "typedef-type",
             "sequence": true,
             "generic": "sequence",
             "nullable": false,
             "union": false,
             "idlType": {
+                "type": "typedef-type",
                 "sequence": false,
                 "generic": null,
                 "nullable": false,
@@ -70,6 +74,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -86,6 +91,7 @@
                 "inherit": false,
                 "readonly": false,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -111,6 +117,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -128,6 +135,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -141,6 +149,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -160,6 +169,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -173,6 +183,7 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -191,6 +202,7 @@
     {
         "type": "typedef",
         "idlType": {
+            "type": "typedef-type",
             "sequence": false,
             "generic": null,
             "nullable": false,
diff --git a/resources/webidl2/test/syntax/json/typesuffixes.json b/resources/webidl2/test/syntax/json/typesuffixes.json
index 0b30830..be0b1f2 100644
--- a/resources/webidl2/test/syntax/json/typesuffixes.json
+++ b/resources/webidl2/test/syntax/json/typesuffixes.json
@@ -12,6 +12,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -25,11 +26,13 @@
                         "variadic": false,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": true,
                             "generic": "sequence",
                             "nullable": true,
                             "union": false,
                             "idlType": {
+                                "type": "argument-type",
                                 "sequence": false,
                                 "generic": null,
                                 "nullable": true,
diff --git a/resources/webidl2/test/syntax/json/uniontype.json b/resources/webidl2/test/syntax/json/uniontype.json
index c4725d4..87735c7 100644
--- a/resources/webidl2/test/syntax/json/uniontype.json
+++ b/resources/webidl2/test/syntax/json/uniontype.json
@@ -1,118 +1,129 @@
 [
-   {
-      "partial": false,
-      "members": [
-         {
-            "idlType": {
-               "idlType": [
-                  {
-                     "union": false,
-                     "sequence": false,
-                     "generic": null,
-                     "idlType": "float",
-                     "nullable": false
-                  },
-                  {
-                     "idlType": [
+    {
+        "type": "interface",
+        "name": "Union",
+        "partial": false,
+        "members": [
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": true,
+                    "idlType": [
                         {
-                           "nullable": false,
-                           "idlType": "Date",
-                           "sequence": false,
-                           "generic": null,
-                           "union": false
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "float"
                         },
                         {
-                           "nullable": false,
-                           "idlType": "Event",
-                           "generic": null,
-                           "sequence": false,
-                           "union": false
-                        }
-                     ],
-                     "nullable": false,
-                     "sequence": false,
-                     "generic": null,
-                     "union": true
-                  },
-                  {
-                     "generic": null,
-                     "sequence": false,
-                     "idlType": [
-                        {
-                           "union": false,
-                           "sequence": false,
-                           "generic": null,
-                           "nullable": false,
-                           "idlType": "Node"
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": true,
+                            "idlType": [
+                                {
+                                    "type": null,
+                                    "sequence": false,
+                                    "generic": null,
+                                    "nullable": false,
+                                    "union": false,
+                                    "idlType": "Date"
+                                },
+                                {
+                                    "type": null,
+                                    "sequence": false,
+                                    "generic": null,
+                                    "nullable": false,
+                                    "union": false,
+                                    "idlType": "Event"
+                                }
+                            ]
                         },
                         {
-                           "nullable": false,
-                           "idlType": "DOMString",
-                           "sequence": false,
-                           "generic": null,
-                           "union": false
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": true,
+                            "union": true,
+                            "idlType": [
+                                {
+                                    "type": null,
+                                    "sequence": false,
+                                    "generic": null,
+                                    "nullable": false,
+                                    "union": false,
+                                    "idlType": "Node"
+                                },
+                                {
+                                    "type": null,
+                                    "sequence": false,
+                                    "generic": null,
+                                    "nullable": false,
+                                    "union": false,
+                                    "idlType": "DOMString"
+                                }
+                            ]
                         }
-                     ],
-                     "nullable": true,
-                     "union": true
-                  }
-               ],
-               "nullable": false,
-               "generic": null,
-               "sequence": false,
-               "union": true
+                    ]
+                },
+                "name": "test",
+                "extAttrs": []
             },
-            "name": "test",
-            "inherit": false,
-            "type": "attribute",
-            "extAttrs": [],
-            "readonly": false,
-            "stringifier": false,
-            "static": false
-         },
-         {
-            "readonly": false,
-            "extAttrs": [],
-            "stringifier": false,
-            "static": false,
-            "name": "test2",
-            "idlType": {
-               "nullable": false,
-               "idlType": [
-                  {
-                     "extAttrs": [
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": false,
+                "idlType": {
+                    "type": "attribute-type",
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": true,
+                    "idlType": [
                         {
-                           "name": "EnforceRange",
-                           "arguments": null,
-                           "type": "extended-attribute",
-                           "rhs": null
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "long",
+                            "extAttrs": [
+                                {
+                                    "name": "EnforceRange",
+                                    "arguments": null,
+                                    "type": "extended-attribute",
+                                    "rhs": null
+                                }
+                            ]
+                        },
+                        {
+                            "type": null,
+                            "sequence": false,
+                            "generic": null,
+                            "nullable": false,
+                            "union": false,
+                            "idlType": "Date"
                         }
-                     ],
-                     "nullable": false,
-                     "idlType": "long",
-                     "generic": null,
-                     "sequence": false,
-                     "union": false
-                  },
-                  {
-                     "union": false,
-                     "sequence": false,
-                     "generic": null,
-                     "idlType": "Date",
-                     "nullable": false
-                  }
-               ],
-               "generic": null,
-               "sequence": false,
-               "union": true
-            },
-            "inherit": false,
-            "type": "attribute"
-         }
-      ],
-      "inheritance": null,
-      "name": "Union",
-      "extAttrs": [],
-      "type": "interface"
-   }
+                    ]
+                },
+                "name": "test2",
+                "extAttrs": []
+            }
+        ],
+        "inheritance": null,
+        "extAttrs": []
+    }
 ]
diff --git a/resources/webidl2/test/syntax/json/variadic-operations.json b/resources/webidl2/test/syntax/json/variadic-operations.json
index a9560da..53b02df 100644
--- a/resources/webidl2/test/syntax/json/variadic-operations.json
+++ b/resources/webidl2/test/syntax/json/variadic-operations.json
@@ -11,6 +11,7 @@
                 "inherit": false,
                 "readonly": true,
                 "idlType": {
+                    "type": "attribute-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -28,6 +29,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -41,6 +43,7 @@
                         "variadic": true,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
@@ -60,6 +63,7 @@
                 "static": false,
                 "stringifier": false,
                 "idlType": {
+                    "type": "return-type",
                     "sequence": false,
                     "generic": null,
                     "nullable": false,
@@ -73,6 +77,7 @@
                         "variadic": true,
                         "extAttrs": [],
                         "idlType": {
+                            "type": "argument-type",
                             "sequence": false,
                             "generic": null,
                             "nullable": false,
diff --git a/resources/webidl2/test/util/acquire.js b/resources/webidl2/test/util/acquire.js
new file mode 100644
index 0000000..6f37dd6
--- /dev/null
+++ b/resources/webidl2/test/util/acquire.js
@@ -0,0 +1,8 @@
+"use strict";
+
+const { collect } = require("./collect");
+const fs = require("fs");
+
+for (const test of collect("syntax")) {
+  fs.writeFileSync(test.jsonPath, `${JSON.stringify(test.ast, null, 4)}\n`)
+}
diff --git a/resources/webidl2/test/util/collect.js b/resources/webidl2/test/util/collect.js
new file mode 100644
index 0000000..7e3d9d3
--- /dev/null
+++ b/resources/webidl2/test/util/collect.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const wp = require("../../lib/webidl2");
+const pth = require("path");
+const fs = require("fs");
+const jdp = require("jsondiffpatch");
+
+/**
+ * Collects test items from the specified directory
+ * @param {string} base
+ */
+function* collect(base, { expectError } = {}) {
+  base = pth.join(__dirname, "..", base);
+  const dir = pth.join(base, "idl");
+  const idls = fs.readdirSync(dir)
+    .filter(it => (/\.widl$/).test(it))
+    .map(it => pth.join(dir, it));
+
+  for (const path of idls) {
+    const optFile = pth.join(base, "opt", pth.basename(path)).replace(".widl", ".json");
+    let opt;
+    if (fs.existsSync(optFile))
+      opt = JSON.parse(fs.readFileSync(optFile, "utf8"));
+
+    try {
+      const ast = wp.parse(fs.readFileSync(path, "utf8").replace(/\r\n/g, "\n"), opt);
+      yield new TestItem({ ast, path, opt });
+    }
+    catch (error) {
+      if (expectError) {
+        yield new TestItem({ path, error });
+      }
+      else {
+        throw error;
+      }
+    }
+  }
+};
+
+
+class TestItem {
+  constructor({ ast, path, error, opt }) {
+    this.ast = ast;
+    this.path = path;
+    this.error = error;
+    this.opt = opt;
+    this.jsonPath = pth.join(pth.dirname(path), "../json", pth.basename(path).replace(".widl", ".json"));
+  }
+
+  readJSON() {
+    return JSON.parse(fs.readFileSync(this.jsonPath, "utf8"));
+  }
+
+  diff(target = this.readJSON()) {
+    return jdp.diff(target, this.ast);
+  }
+}
+
+module.exports.collect = collect;
diff --git a/resources/webidl2/test/writer.js b/resources/webidl2/test/writer.js
new file mode 100644
index 0000000..e84076b
--- /dev/null
+++ b/resources/webidl2/test/writer.js
@@ -0,0 +1,23 @@
+"use strict";
+
+const { collect } = require("./util/collect");
+const wp = require("../lib/webidl2");
+const writer = require("../lib/writer");
+const expect = require("expect");
+const debug = true;
+
+describe("Rewrite and parses all of the IDLs to produce the same ASTs", () => {
+  for (const test of collect("syntax")) {
+    it(`should produce the same AST for ${test.path}`, () => {
+      try {
+        const diff = test.diff(wp.parse(writer.write(test.ast), test.opt));
+        if (diff && debug) console.log(JSON.stringify(diff, null, 4));
+        expect(diff).toBe(undefined);
+      }
+      catch (e) {
+        console.log(e.toString());
+        throw e;
+      }
+    });
+  }
+});
diff --git a/server-timing/navigation_timing_idl.html b/server-timing/navigation_timing_idl.html
new file mode 100644
index 0000000..290bb88
--- /dev/null
+++ b/server-timing/navigation_timing_idl.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<head>
+    <meta charset='utf-8' />
+    <script src="/resources/testharness.js"></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script>
+      setup({explicit_done: true})
+      window.addEventListener('load', function(){
+        assert_not_equals(typeof performance.getEntriesByType('navigation')[0].serverTiming, 'undefined',
+          'An instance of `PerformanceNavigationTiming` should have a `serverTiming` attribute.')
+        done()
+      })
+    </script>
+</head>
diff --git a/server-timing/resource_timing_idl.html b/server-timing/resource_timing_idl.html
new file mode 100644
index 0000000..d2c3c92
--- /dev/null
+++ b/server-timing/resource_timing_idl.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<head>
+    <meta charset='utf-8' />
+    <script src="/resources/testharness.js"></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script>
+      setup({explicit_done: true})
+      window.addEventListener('load', function(){
+        assert_not_equals(typeof performance.getEntriesByType('resource')[0].serverTiming, 'undefined',
+          'An instance of `PerformanceResourceTiming` should have a `serverTiming` attribute.')
+        done()
+      })
+    </script>
+</head>
diff --git a/server-timing/server_timing_header-parsing.html b/server-timing/server_timing_header-parsing.html
index 4049c3d..50396ed 100644
--- a/server-timing/server_timing_header-parsing.html
+++ b/server-timing/server_timing_header-parsing.html
@@ -19,7 +19,6 @@
       }
       function runTests() {
         tests.forEach(function({url, expectedResults}) {
-          debugger;
           const {serverTiming} = performance.getEntriesByName(url)[0]
           const fileName = url.substring(url.lastIndexOf('/') + 1)
 
diff --git a/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000..ee574d2
--- /dev/null
+++ b/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+    const params = new URL(event.request.url).searchParams;
+    if (params.has('ignore')) {
+      return;
+    }
+    if (!params.has('name')) {
+      event.respondWith(Promise.reject(TypeError('No name is provided.')));
+      return;
+    }
+
+    event.respondWith(Promise.resolve().then(async () => {
+        const name = params.get('name');
+        await caches.delete('foo');
+        const cache = await caches.open('foo');
+        await cache.put(event.request, new Response('hello'));
+        const keys = await cache.keys();
+
+        const original = event.request[name];
+        const stored = keys[0][name];
+        return new Response(`original: ${original}, stored: ${stored}`);
+      }));
+  });
diff --git a/service-workers/cache-storage/serviceworker/cache-abort.https.html b/service-workers/cache-storage/serviceworker/cache-abort.https.html
index b4f203b..5a8b539 100644
--- a/service-workers/cache-storage/serviceworker/cache-abort.https.html
+++ b/service-workers/cache-storage/serviceworker/cache-abort.https.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <title>Cache Storage: Abort</title>
 <link rel="help" href="https://fetch.spec.whatwg.org/#request-signal">
+<meta name="timeout" content="long">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="../../service-worker/resources/test-helpers.sub.js"></script>
diff --git a/service-workers/cache-storage/serviceworker/cache-keys-attributes-for-service-worker.https.html b/service-workers/cache-storage/serviceworker/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000..2024076
--- /dev/null
+++ b/service-workers/cache-storage/serviceworker/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = '../resources/cache-keys-attributes-for-service-worker.js';
+
+promise_test(async (t) => {
+    const scope = '../resources/blank.html?name=isReloadNavigation';
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: false, stored: false');
+      await new Promise((resolve) => {
+        frame.onload = resolve;
+        frame.contentWindow.location.reload();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: true, stored: true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+}, 'Request.IsReloadNavigation should persist.');
+</script>
diff --git a/service-workers/cache-storage/window/cache-abort.https.html b/service-workers/cache-storage/window/cache-abort.https.html
index 935023d..405d34d 100644
--- a/service-workers/cache-storage/window/cache-abort.https.html
+++ b/service-workers/cache-storage/window/cache-abort.https.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <title>Cache Storage: Abort</title>
 <link rel="help" href="https://fetch.spec.whatwg.org/#request-signal">
+<meta name="timeout" content="long">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="../resources/test-helpers.js"></script>
diff --git a/service-workers/cache-storage/worker/cache-abort.https.html b/service-workers/cache-storage/worker/cache-abort.https.html
index ccd7191..68bbade 100644
--- a/service-workers/cache-storage/worker/cache-abort.https.html
+++ b/service-workers/cache-storage/worker/cache-abort.https.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <title>>Cache Storage: Abort</title>
 <link rel="help" href="https://fetch.spec.whatwg.org/#request-signal">
+<meta name="timeout" content="long">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script>
diff --git a/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/service-workers/service-worker/Service-Worker-Allowed-header.https.html
new file mode 100644
index 0000000..316067c
--- /dev/null
+++ b/service-workers/service-worker/Service-Worker-Allowed-header.https.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker-Allowed header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const host_info = get_host_info();
+
+// Returns a URL for a service worker script whose Service-Worker-Allowed
+// header value is set to |allowed_path|. If |origin| is specified, that origin
+// is used.
+function build_script_url(allowed_path, origin) {
+  const script = 'resources/empty-worker.js';
+  const url = origin ? `${origin}${base_path()}${script}` : script;
+  return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`;
+}
+
+promise_test(async t => {
+  const script = build_script_url('/allowed-path');
+  const scope = '/allowed-path';
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+  assert_equals(registration.scope, normalizeURL(scope));
+  return registration.unregister();
+}, 'Registering within Service-Worker-Allowed path');
+
+promise_test(async t => {
+  const script = build_script_url(new URL('/allowed-path', document.location));
+  const scope = '/allowed-path';
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+  assert_equals(registration.scope, normalizeURL(scope));
+  return registration.unregister();
+}, 'Registering within Service-Worker-Allowed path (absolute URL)');
+
+promise_test(async t => {
+  const script = build_script_url('../allowed-path-with-parent');
+  const scope = 'allowed-path-with-parent';
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+  assert_equals(registration.scope, normalizeURL(scope));
+  return registration.unregister();
+}, 'Registering within Service-Worker-Allowed path with parent reference');
+
+promise_test(async t => {
+  const script = build_script_url('../allowed-path');
+  const scope = '/disallowed-path';
+  await service_worker_unregister(t, scope);
+  return promise_rejects(t,
+      'SecurityError',
+      navigator.serviceWorker.register(script, {scope: scope}),
+      'register should fail');
+}, 'Registering outside Service-Worker-Allowed path');
+
+promise_test(async t => {
+  const script = build_script_url('../allowed-path-with-parent');
+  const scope = '/allowed-path-with-parent';
+  await service_worker_unregister(t, scope);
+  return promise_rejects(t,
+      'SecurityError',
+      navigator.serviceWorker.register(script, {scope: scope}),
+      'register should fail');
+}, 'Registering outside Service-Worker-Allowed path with parent reference');
+
+promise_test(async t => {
+  const script = build_script_url(
+      host_info.HTTPS_REMOTE_ORIGIN + '/');
+  const scope = 'resources/this-scope-is-normally-allowed'
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+  assert_equals(registration.scope, normalizeURL(scope));
+  return registration.unregister();
+}, 'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope');
+
+promise_test(async t => {
+  const script = build_script_url(
+      host_info.HTTPS_REMOTE_ORIGIN + '/');
+  const scope = '/this-scope-is-normally-disallowed'
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+  assert_equals(registration.scope, normalizeURL(scope));
+  return registration.unregister();
+}, 'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope');
+
+promise_test(async t => {
+  const script = build_script_url(
+      host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/',
+      host_info.HTTPS_REMOTE_ORIGIN);
+  const scope = '/cross-origin/';
+  await service_worker_unregister(t, scope);
+  return promise_rejects(t,
+      'SecurityError',
+      navigator.serviceWorker.register(script, {scope: scope}),
+      'register should fail');
+}, 'Service-Worker-Allowed is cross-origin to page, same-origin to script');
+
+</script>
diff --git a/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
new file mode 100644
index 0000000..cf1fecc
--- /dev/null
+++ b/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+promise_test(async (t) => {
+  const scope = 'resources/simple.html?isReloadNavigation';
+
+  const reg = await service_worker_unregister_and_register(t, worker, scope);
+  await wait_for_state(t, reg.installing, 'activated');
+  const frame = await with_iframe(scope);
+  assert_equals(frame.contentDocument.body.textContent,
+                'method = GET, isReloadNavigation = false');
+  await new Promise((resolve) => {
+    frame.addEventListener('load', resolve);
+    frame.contentDocument.body.innerText =
+      'Reload this frame manually!';
+  });
+  assert_equals(frame.contentDocument.body.textContent,
+      'method = GET, isReloadNavigation = true');
+  frame.remove();
+  await reg.unregister();
+}, 'FetchEvent#request.isReloadNavigation is true for manual reload.');
+
+</script>
+</body>
+</html>
diff --git a/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
new file mode 100644
index 0000000..a349f07
--- /dev/null
+++ b/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>.
+   Once you see &quot;method = GET,...&quot; in the page, reload the page.
+   You will see &quot;method = GET, isReloadNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/service-workers/service-worker/fetch-event-redirect.https.html b/service-workers/service-worker/fetch-event-redirect.https.html
index c72be76..c42d7f7 100644
--- a/service-workers/service-worker/fetch-event-redirect.https.html
+++ b/service-workers/service-worker/fetch-event-redirect.https.html
@@ -229,10 +229,14 @@
       redirect: 'manual',
       mode: 'no-cors'
     },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
     should_reject: false
   });
 }, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
-   'no-cors without credentials should succeed interception ' +
+   'no-cors without credentials should succeed opaqueredirect interception ' +
    'and response should not be redirected');
 
 promise_test(function(t) {
@@ -246,10 +250,14 @@
       redirect: 'manual',
       mode: 'no-cors'
     },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
     should_reject: false
   });
 }, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
-   'cors without credentials should succeed interception ' +
+   'cors without credentials should succeed opaqueredirect interception ' +
    'and response should not be redirected');
 
 promise_test(function(t) {
@@ -382,10 +390,14 @@
       redirect: 'manual',
       mode: 'no-cors'
     },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
     should_reject: false
   });
 }, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
-   'no-cors with credentials should succeed interception ' +
+   'no-cors with credentials should succeed opaqueredirect interception ' +
    'and response should not be redirected');
 
 promise_test(function(t) {
@@ -399,10 +411,14 @@
       redirect: 'manual',
       mode: 'no-cors'
     },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
     should_reject: false
   });
 }, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
-   'cors with credentials should succeed interception ' +
+   'cors with credentials should succeed opaqueredirect interception ' +
    'and response should not be redirected');
 
 promise_test(function(t) {
diff --git a/service-workers/service-worker/fetch-event.https.html b/service-workers/service-worker/fetch-event.https.html
index 12b0932..02f8d0e 100644
--- a/service-workers/service-worker/fetch-event.https.html
+++ b/service-workers/service-worker/fetch-event.https.html
@@ -6,6 +6,9 @@
 <body>
 <script>
 var worker = 'resources/fetch-event-test-worker.js';
+function wait(ms) {
+    return new Promise(resolve => step_timeout(resolve, ms));
+}
 
 async_test(function(t) {
     const scope = 'resources/simple.html?headers';
@@ -629,5 +632,143 @@
     frame.remove();
     await service_worker_unregister_and_done(t, scope);
   }, 'Service Worker responds to fetch event with the correct keepalive value');
+
+promise_test(async (t) => {
+    const scope = 'resources/simple.html?isReloadNavigation';
+    let frame;
+    let reg;
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = false');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.location.reload();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+  }, 'FetchEvent#request.isReloadNavigation is true (location.reload())');
+
+promise_test(async (t) => {
+    const scope = 'resources/simple.html?isReloadNavigation';
+    let frame;
+    let reg;
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = false');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.history.go(0);
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+  }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))');
+
+promise_test(async (t) => {
+    const scope = 'resources/simple.html?isReloadNavigation';
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = false');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        const form = frame.contentDocument.createElement('form');
+        form.method = 'POST';
+        form.name = 'form';
+        form.action = new Request(scope).url;
+        frame.contentDocument.body.appendChild(form);
+        form.submit();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = POST, isReloadNavigation = false');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.location.reload();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = POST, isReloadNavigation = true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+  }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())');
+
+promise_test(async (t) => {
+    const scope = 'resources/simple.html?isReloadNavigation';
+    const anotherUrl = new Request('resources/simple.html').url;
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = false');
+      // Use step_timeout(0) to ensure the history entry is created for Blink
+      // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+      await wait(0);
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.src = anotherUrl;
+      });
+      assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.history.go(-1);
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = false');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.history.go(0);
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'method = GET, isReloadNavigation = true');
+      await new Promise((resolve) => {
+        frame.addEventListener('load', resolve);
+        frame.contentWindow.history.go(1);
+      });
+      assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+  }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)');
+
 </script>
 </body>
diff --git a/service-workers/service-worker/fetch-request-resources.https.html b/service-workers/service-worker/fetch-request-resources.https.html
index 336e6ed..50421b4 100644
--- a/service-workers/service-worker/fetch-request-resources.https.html
+++ b/service-workers/service-worker/fetch-request-resources.https.html
@@ -25,6 +25,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: 'image',
       message: 'Image load (url:' +
                actual_url + ' cross_origin:' + cross_origin + ')'
     };
@@ -41,6 +42,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: 'script',
       message: 'Script load (url:' +
                actual_url + ' cross_origin:' + cross_origin + ')'
     };
@@ -57,6 +59,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: 'style',
       message: 'CSS load (url:' +
                actual_url + ' cross_origin:' + cross_origin + ')'
     };
@@ -72,6 +75,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: 'font',
       message: 'FontFace load (url:' + actual_url + ')'
     };
   frame.contentWindow.load_font(actual_url);
@@ -86,6 +90,7 @@
       credentials: 'include',
       redirect: 'follow',
       integrity: expected_integrity,
+      destination: 'script',
       message: 'Script load (url:' + actual_url + ')'
     };
   frame.contentWindow.load_script_with_integrity(actual_url, integrity);
@@ -100,6 +105,7 @@
       credentials: 'include',
       redirect: 'follow',
       integrity: expected_integrity,
+      destination: 'style',
       message: 'CSS load (url:' + actual_url + ')'
     };
   frame.contentWindow.load_css_with_integrity(actual_url, integrity);
@@ -114,6 +120,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: '',
       message: 'fetch (url:' + actual_url + ' mode:' + mode + ' credentials:' +
                credentials + ')'
     };
@@ -131,6 +138,7 @@
       credentials: expected_credentials,
       redirect: 'follow',
       integrity: '',
+      destination: 'audio',
       message: 'Audio load (url:' + actual_url + ' cross_origin:' +
                cross_origin + ')'
     };
@@ -138,6 +146,23 @@
   return add_promise_to_test(actual_url);
 }
 
+
+function video_test(frame, url, cross_origin,
+                    expected_mode, expected_credentials) {
+  var actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'video',
+      message: 'Video load (url:' + actual_url + ' cross_origin:' +
+               cross_origin + ')'
+    };
+  frame.contentWindow.load_video(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
 promise_test(function(t) {
     var SCOPE = 'resources/fetch-request-resources-iframe.https.html';
     var SCRIPT = 'resources/fetch-request-resources-worker.js';
@@ -183,6 +208,10 @@
                     result.integrity, expected.integrity,
                     'integrity of ' + expected.message +  ' must be ' +
                     expected.integrity + '.');
+                  assert_equals(
+                    result.destination, expected.destination,
+                    'destination of ' + expected.message +  ' must be ' +
+                    expected.destination + '.');
                 }, expected.message);
                 expected.resolve();
                 delete expected_results[result.url];
@@ -268,6 +297,13 @@
         await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
         await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
 
+        await video_test(f, LOCAL_URL, '', 'no-cors', 'include');
+        await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+        await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+        await video_test(f, REMOTE_URL, '', 'no-cors', 'include');
+        await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+        await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
         frame.remove();
         service_worker_unregister(t, SCOPE);
       }).catch(unreached_rejection(t));
diff --git a/service-workers/service-worker/resources/fetch-event-test-worker.js b/service-workers/service-worker/resources/fetch-event-test-worker.js
index 65025d9..a313094 100644
--- a/service-workers/service-worker/resources/fetch-event-test-worker.js
+++ b/service-workers/service-worker/resources/fetch-event-test-worker.js
@@ -129,6 +129,14 @@
   event.respondWith(new Response(event.request.keepalive));
 }
 
+function handleIsReloadNavigation(event) {
+  const request = event.request;
+  const body =
+    `method = ${request.method}, ` +
+    `isReloadNavigation = ${request.isReloadNavigation}`;
+  event.respondWith(new Response(body));
+}
+
 self.addEventListener('fetch', function(event) {
     var url = event.request.url;
     var handlers = [
@@ -151,6 +159,7 @@
       { pattern: '?integrity', fn: handleIntegrity },
       { pattern: '?request-body', fn: handleRequestBody },
       { pattern: '?keepalive', fn: handleKeepalive },
+      { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation },
     ];
 
     var handler = null;
diff --git a/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
index 2e5d7df..fca5722 100644
--- a/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
+++ b/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
@@ -71,5 +71,14 @@
   audio.src = url;
   document.body.appendChild(audio);
 }
+
+function load_video(url, cross_origin) {
+  var video = document.createElement('video');
+  if (cross_origin != '') {
+    video.crossOrigin = cross_origin;
+  }
+  video.src = url;
+  document.body.appendChild(video);
+}
 </script>
 </body>
diff --git a/service-workers/service-worker/resources/fetch-request-resources-worker.js b/service-workers/service-worker/resources/fetch-request-resources-worker.js
index e732da0..d85f714 100644
--- a/service-workers/service-worker/resources/fetch-request-resources-worker.js
+++ b/service-workers/service-worker/resources/fetch-request-resources-worker.js
@@ -19,7 +19,8 @@
         mode: event.request.mode,
         redirect: event.request.redirect,
         credentials: event.request.credentials,
-        integrity: event.request.integrity
+        integrity: event.request.integrity,
+        destination: event.request.destination
       });
     event.respondWith(Promise.reject());
   });
diff --git a/service-workers/service-worker/resources/install-worker.html b/service-workers/service-worker/resources/install-worker.html
new file mode 100644
index 0000000..ed20cd4
--- /dev/null
+++ b/service-workers/service-worker/resources/install-worker.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Loading...</p>
+<script>
+async function install() {
+  let script;
+  for (const q of location.search.slice(1).split('&')) {
+    if (q.split('=')[0] === 'script') {
+      script = q.split('=')[1];
+    }
+  }
+  const scope = location.href;
+  const reg = await navigator.serviceWorker.register(script, {scope});
+  await navigator.serviceWorker.ready;
+  location.reload();
+}
+
+install();
+</script>
+</body>
+</html>
diff --git a/service-workers/service-worker/resources/scope1/redirect.py b/service-workers/service-worker/resources/scope1/redirect.py
index 0663ca0..bb4c874 100644
--- a/service-workers/service-worker/resources/scope1/redirect.py
+++ b/service-workers/service-worker/resources/scope1/redirect.py
@@ -1,5 +1,6 @@
 import os
-import sys
+import imp
 # Use the file from the parent directory.
-sys.path.append(os.path.dirname(os.path.dirname(__file__)))
-from redirect import main
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                              os.path.basename(__file__)))
+main = mod.main
diff --git a/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
index 99db134..bb4c874 100644
--- a/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
+++ b/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
@@ -1,5 +1,6 @@
 import os
-import sys
+import imp
 # Use the file from the parent directory.
-sys.path.append(os.path.dirname(os.path.dirname(__file__)))
-from worker_interception_redirect_webworker import main
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                              os.path.basename(__file__)))
+main = mod.main
diff --git a/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
index b815442..f5ba5a3 100644
--- a/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
+++ b/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
@@ -31,9 +31,18 @@
     return;
   }
 
-  // (3) The worker does a fetch() to simple.txt. Indicate that this service
+  // (3) The worker does an importScripts() to import-scripts-echo.py. Indicate
+  // that this service worker handled the request.
+  if (evt.request.url.indexOf('import-scripts-echo.py') != -1) {
+    const msg = encodeURIComponent(`${name} saw importScripts from the worker`);
+    evt.respondWith(fetch(`import-scripts-echo.py?msg=${msg}`));
+    return;
+  }
+
+  // (4) The worker does a fetch() to simple.txt. Indicate that this service
   // worker handled the request.
   if (evt.request.url.indexOf('simple.txt') != -1) {
     evt.respondWith(new Response(`${name} saw the fetch from the worker`));
+    return;
   }
 });
diff --git a/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
index 7863af7..d602f23 100644
--- a/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
+++ b/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
@@ -18,11 +18,23 @@
 if (!greeting)
   greeting = 'the shared worker script was served from network';
 
+// Call importScripts() which fills |echo_output| with a string indicating
+// whether a service worker intercepted the importScripts() request.
+let echo_output;
+const import_scripts_msg = encodeURIComponent(
+    'importScripts: served from network');
+const import_scripts_url =
+    new URL(`import-scripts-echo.py?msg=${import_scripts_msg}`, resources_url);
+importScripts(import_scripts_url);
+const import_scripts_greeting = echo_output;
+
 self.onconnect = async function(e) {
   const port = e.ports[0];
   port.start();
   port.postMessage(greeting);
 
+  port.postMessage(import_scripts_greeting);
+
   const fetch_url = new URL('simple.txt', resources_url);
   const response = await fetch(fetch_url);
   const text = await response.text();
diff --git a/service-workers/service-worker/update-result.https.html b/service-workers/service-worker/update-result.https.html
new file mode 100644
index 0000000..d8ed94f
--- /dev/null
+++ b/service-workers/service-worker/update-result.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(async function(t) {
+  const script = './resources/empty.js';
+  const scope = './resources/empty.html?update-result';
+
+  let reg = await navigator.serviceWorker.register(script, { scope });
+  t.add_cleanup(async _ => await reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  let result = await reg.update();
+  assert_true(result instanceof ServiceWorkerRegistration,
+              'update() should resolve a ServiceWorkerRegistration');
+}, 'ServiceWorkerRegistration.update() should resolve a registration object');
+
+</script>
+</body>
diff --git a/service-workers/service-worker/worker-interception-redirect.https.html b/service-workers/service-worker/worker-interception-redirect.https.html
index 4e79eac..654495f 100644
--- a/service-workers/service-worker/worker-interception-redirect.https.html
+++ b/service-workers/service-worker/worker-interception-redirect.https.html
@@ -32,8 +32,8 @@
 // 3. The request is redirected to scope2 or out-of-scope.
 // 4. The worker posts message to the page describing where the final response
 //   was served from (service worker or network).
-// 5. The worker does a fetch(), and posts back the response, which describes
-//   where the fetch response was served from.
+// 5. The worker does an importScripts() and fetch(), and posts back the
+//   responses, which describe where the responses where served from.
 //
 // Currently this only tests shared worker but dedicated worker tests should be
 // added in a future patch.
@@ -109,7 +109,8 @@
 
 function worker_redirect_test(worker_url,
                               expected_main_resource_message,
-                              expected_subresource_message,
+                              expected_import_scripts_message,
+                              expected_fetch_message,
                               description) {
   promise_test(async t => {
     // Create a frame to load the worker from. This way we can remove the frame
@@ -126,43 +127,44 @@
     const data = await get_message_from_worker(w);
     assert_equals(data, expected_main_resource_message);
 
-    // The worker does a fetch() after it starts up. Expect a message from the
-    // worker indicating which service worker provided the response for the
-    // fetch(), if any.
-    //
-    // Note: for some reason, Firefox would pass all these tests if a
-    // postMessage ping/pong step is added before the fetch(). I.e., if the
-    // page does postMessage() and the worker does fetch() in response to the
-    // ping, the fetch() is properly intercepted. See
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=1452528. (Chrome can't pass
-    // the tests either way.)
-    const message = get_message_from_worker(w);
-    const data2 = await message;
-    assert_equals(data2, expected_subresource_message);
+    // The worker does an importScripts(). Expect a message from the worker
+    // indicating which service worker provided the response for the
+    // importScripts(), if any.
+    const import_scripts_message = await get_message_from_worker(w);
+    assert_equals(import_scripts_message, expected_import_scripts_message);
+
+    // The worker does a fetch(). Expect a message from the worker indicating
+    // which service worker provided the response for the fetch(), if any.
+    const fetch_message = await get_message_from_worker(w);
+    assert_equals(fetch_message, expected_fetch_message);
   }, description);
 }
 
 worker_redirect_test(
     build_worker_url('network', 'scope2'),
     'the shared worker script was served from network',
+    'sw1 saw importScripts from the worker',
     'fetch(): sw1 saw the fetch from the worker',
     'request to sw1 scope gets network redirect to sw2 scope');
 
 worker_redirect_test(
     build_worker_url('network', 'out-scope'),
     'the shared worker script was served from network',
+    'sw1 saw importScripts from the worker',
     'fetch(): sw1 saw the fetch from the worker',
     'request to sw1 scope gets network redirect to out-of-scope');
 
 worker_redirect_test(
     build_worker_url('serviceworker', 'scope2'),
     'sw2 saw the request for the worker script',
+    'sw2 saw importScripts from the worker',
     'fetch(): sw2 saw the fetch from the worker',
     'request to sw1 scope gets service-worker redirect to sw2 scope');
 
 worker_redirect_test(
     build_worker_url('serviceworker', 'out-scope'),
     'the shared worker script was served from network',
+    'sw1 saw importScripts from the worker',
     'fetch(): sw1 saw the fetch from the worker',
     'request to sw1 scope gets service-worker redirect to out-of-scope');
 </script>
diff --git a/shadow-dom/Extensions-to-Event-Interface.html b/shadow-dom/Extensions-to-Event-Interface.html
index 37d07a7..52cda0b 100644
--- a/shadow-dom/Extensions-to-Event-Interface.html
+++ b/shadow-dom/Extensions-to-Event-Interface.html
@@ -14,11 +14,6 @@
 <script>
 
 test(function () {
-    assert_true('composedPath' in Event.prototype);
-    assert_true('composedPath' in new Event('my-event'));
-}, 'composedPath() must exist on Event');
-
-test(function () {
     var event = new Event('my-event');
     assert_array_equals(event.composedPath(), []);
 }, 'composedPath() must return an empty array when the event has not been dispatched');
@@ -30,11 +25,6 @@
 }, 'composedPath() must return an empty array when the event is no longer dispatched');
 
 test(function () {
-    assert_true('composed' in Event.prototype);
-    assert_true('composed' in new Event('my-event'));
-}, 'composed must exist on Event');
-
-test(function () {
     var event = new Event('my-event');
     assert_false(event.composed);
 }, 'composed on EventInit must default to false');
diff --git a/shadow-dom/resources/event-path-test-helpers.js b/shadow-dom/resources/event-path-test-helpers.js
index 17d6fff..f0e8ec3 100644
--- a/shadow-dom/resources/event-path-test-helpers.js
+++ b/shadow-dom/resources/event-path-test-helpers.js
@@ -16,9 +16,6 @@
                 eventPath.push(this.label);
                 relatedTargets.push(event.relatedTarget ? event.relatedTarget.label : null);
 
-                if (!event.composedPath) // Don't fail all tests just for the lack of composedPath.
-                    return;
-
                 pathAtTargets.push(event.composedPath().map(function (node) { return node.label; }));
                 targets.push(event.target);
             }).bind(node));
diff --git a/streams/readable-byte-streams/constructor.dedicatedworker.html b/streams/readable-byte-streams/constructor.dedicatedworker.html
new file mode 100644
index 0000000..aebe97e
--- /dev/null
+++ b/streams/readable-byte-streams/constructor.dedicatedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js dedicated worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new Worker('constructor.js'));
+</script>
diff --git a/streams/readable-byte-streams/constructor.html b/streams/readable-byte-streams/constructor.html
new file mode 100644
index 0000000..a548e08
--- /dev/null
+++ b/streams/readable-byte-streams/constructor.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js browser context wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="../resources/constructor-ordering.js"></script>
+
+<script src="constructor.js"></script>
diff --git a/streams/readable-byte-streams/constructor.js b/streams/readable-byte-streams/constructor.js
new file mode 100644
index 0000000..3405e23
--- /dev/null
+++ b/streams/readable-byte-streams/constructor.js
@@ -0,0 +1,53 @@
+'use strict';
+
+if (self.importScripts) {
+  self.importScripts('/resources/testharness.js');
+  self.importScripts('../resources/constructor-ordering.js');
+}
+
+const operations = [
+  op('get', 'size'),
+  op('get', 'highWaterMark'),
+  op('get', 'type'),
+  op('validate', 'type'),
+  op('validate', 'size'),
+  op('tonumber', 'highWaterMark'),
+  op('validate', 'highWaterMark'),
+  op('get', 'pull'),
+  op('validate', 'pull'),
+  op('get', 'cancel'),
+  op('validate', 'cancel'),
+  op('get', 'autoAllocateChunkSize'),
+  op('tonumber', 'autoAllocateChunkSize'),
+  op('validate', 'autoAllocateChunkSize'),
+  op('get', 'start'),
+  op('validate', 'start')
+];
+
+for (const failureOp of operations) {
+  test(() => {
+    const record = new OpRecorder(failureOp);
+    const underlyingSource = createRecordingObjectWithProperties(record, ['start', 'pull', 'cancel']);
+
+    // The valid value for "type" is "bytes", so set it separately.
+    defineCheckedProperty(record, underlyingSource, 'type', () => record.check('type') ? 'invalid' : 'bytes');
+
+    // autoAllocateChunkSize is a special case because it has a tonumber step.
+    defineCheckedProperty(record, underlyingSource, 'autoAllocateChunkSize',
+                          () => createRecordingNumberObject(record, 'autoAllocateChunkSize'));
+
+    const strategy = createRecordingStrategy(record);
+
+    try {
+      new ReadableStream(underlyingSource, strategy);
+      assert_unreached('constructor should throw');
+    } catch (e) {
+      assert_equals(typeof e, 'object', 'e should be an object');
+    }
+
+    assert_equals(record.actual(), expectedAsString(operations, failureOp),
+                  'operations should be performed in the right order');
+  }, `ReadableStream constructor should stop after ${failureOp} fails`);
+}
+
+done();
diff --git a/streams/readable-byte-streams/constructor.serviceworker.https.html b/streams/readable-byte-streams/constructor.serviceworker.https.html
new file mode 100644
index 0000000..ddddf06
--- /dev/null
+++ b/streams/readable-byte-streams/constructor.serviceworker.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js service worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+'use strict';
+service_worker_test('constructor.js', 'Service worker test setup');
+</script>
diff --git a/streams/readable-byte-streams/constructor.sharedworker.html b/streams/readable-byte-streams/constructor.sharedworker.html
new file mode 100644
index 0000000..52b1a5d
--- /dev/null
+++ b/streams/readable-byte-streams/constructor.sharedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js shared worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new SharedWorker('constructor.js'));
+</script>
diff --git a/streams/readable-streams/constructor.dedicatedworker.html b/streams/readable-streams/constructor.dedicatedworker.html
new file mode 100644
index 0000000..aebe97e
--- /dev/null
+++ b/streams/readable-streams/constructor.dedicatedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js dedicated worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new Worker('constructor.js'));
+</script>
diff --git a/streams/readable-streams/constructor.html b/streams/readable-streams/constructor.html
new file mode 100644
index 0000000..a548e08
--- /dev/null
+++ b/streams/readable-streams/constructor.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js browser context wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="../resources/constructor-ordering.js"></script>
+
+<script src="constructor.js"></script>
diff --git a/streams/readable-streams/constructor.js b/streams/readable-streams/constructor.js
new file mode 100644
index 0000000..c202f3b
--- /dev/null
+++ b/streams/readable-streams/constructor.js
@@ -0,0 +1,42 @@
+'use strict';
+
+if (self.importScripts) {
+  self.importScripts('/resources/testharness.js');
+  self.importScripts('../resources/constructor-ordering.js');
+}
+
+const operations = [
+  op('get', 'size'),
+  op('get', 'highWaterMark'),
+  op('get', 'type'),
+  op('validate', 'type'),
+  op('validate', 'size'),
+  op('tonumber', 'highWaterMark'),
+  op('validate', 'highWaterMark'),
+  op('get', 'pull'),
+  op('validate', 'pull'),
+  op('get', 'cancel'),
+  op('validate', 'cancel'),
+  op('get', 'start'),
+  op('validate', 'start')
+];
+
+for (const failureOp of operations) {
+  test(() => {
+    const record = new OpRecorder(failureOp);
+    const underlyingSource = createRecordingObjectWithProperties(record, ['type', 'start', 'pull', 'cancel']);
+    const strategy = createRecordingStrategy(record);
+
+    try {
+      new ReadableStream(underlyingSource, strategy);
+      assert_unreached('constructor should throw');
+    } catch (e) {
+      assert_equals(typeof e, 'object', 'e should be an object');
+    }
+
+    assert_equals(record.actual(), expectedAsString(operations, failureOp),
+                  'operations should be performed in the right order');
+  }, `ReadableStream constructor should stop after ${failureOp} fails`);
+}
+
+done();
diff --git a/streams/readable-streams/constructor.serviceworker.https.html b/streams/readable-streams/constructor.serviceworker.https.html
new file mode 100644
index 0000000..ddddf06
--- /dev/null
+++ b/streams/readable-streams/constructor.serviceworker.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js service worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+'use strict';
+service_worker_test('constructor.js', 'Service worker test setup');
+</script>
diff --git a/streams/readable-streams/constructor.sharedworker.html b/streams/readable-streams/constructor.sharedworker.html
new file mode 100644
index 0000000..52b1a5d
--- /dev/null
+++ b/streams/readable-streams/constructor.sharedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js shared worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new SharedWorker('constructor.js'));
+</script>
diff --git a/streams/resources/constructor-ordering.js b/streams/resources/constructor-ordering.js
new file mode 100644
index 0000000..79862e0
--- /dev/null
+++ b/streams/resources/constructor-ordering.js
@@ -0,0 +1,129 @@
+'use strict';
+
+// Helpers for tests that constructors perform getting and validation of properties in the standard order.
+// See ../readable-streams/constructor.js for an example of how to use them.
+
+// Describes an operation on a property. |type| is "get", "validate" or "tonumber". |name| is the name of the property
+// in question. |side| is usually undefined, but is used by TransformStream to distinguish between the readable and
+// writable strategies.
+class Op {
+  constructor(type, name, side) {
+    this.type = type;
+    this.name = name;
+    this.side = side;
+  }
+
+  toString() {
+    return this.side === undefined ? `${this.type} on ${this.name}` : `${this.type} on ${this.name} (${this.side})`;
+  }
+
+  equals(otherOp) {
+    return this.type === otherOp.type && this.name === otherOp.name && this.side === otherOp.side;
+  }
+}
+
+// Provides a concise syntax to create an Op object. |side| is used by TransformStream to distinguish between the two
+// strategies.
+function op(type, name, side = undefined) {
+  return new Op(type, name, side);
+}
+
+// Records a sequence of operations. Also checks each operation against |failureOp| to see if it should fail.
+class OpRecorder {
+  constructor(failureOp) {
+    this.ops = [];
+    this.failureOp = failureOp;
+    this.matched = false;
+  }
+
+  // Record an operation. Returns true if this operation should fail.
+  recordAndCheck(type, name, side = undefined) {
+    const recordedOp = op(type, name, side);
+    this.ops.push(recordedOp);
+    return this.failureOp.equals(recordedOp);
+  }
+
+  // Returns true if validation of this property should fail.
+  check(name, side = undefined) {
+    return this.failureOp.equals(op('validate', name, side));
+  }
+
+  // Returns the sequence of recorded operations as a string.
+  actual() {
+    return this.ops.toString();
+  }
+}
+
+// Creates an object with the list of properties named in |properties|. Every property access will be recorded in
+// |record|, which will also be used to determine whether a particular property access should fail, or whether it should
+// return an invalid value that will fail validation.
+function createRecordingObjectWithProperties(record, properties) {
+  const recordingObject = {};
+  for (const property of properties) {
+    defineCheckedProperty(record, recordingObject, property, () => record.check(property) ? 'invalid' : undefined);
+  }
+  return recordingObject;
+}
+
+// Add a getter to |object| named |property| which throws if op('get', property) should fail, and otherwise calls
+// getter() to get the return value.
+function defineCheckedProperty(record, object, property, getter) {
+  Object.defineProperty(object, property, {
+    get() {
+      if (record.recordAndCheck('get', property)) {
+        throw new Error(`intentional failure of get ${property}`);
+      }
+      return getter();
+    }
+  });
+}
+
+// Similar to createRecordingObjectWithProperties(), but with specific functionality for "highWaterMark" so that numeric
+// conversion can be recorded. Permits |side| to be specified so that TransformStream can distinguish between its two
+// strategies.
+function createRecordingStrategy(record, side = undefined) {
+  return {
+    get size() {
+      if (record.recordAndCheck('get', 'size', side)) {
+        throw new Error(`intentional failure of get size`);
+      }
+      return record.check('size', side) ? 'invalid' : undefined;
+    },
+    get highWaterMark() {
+      if (record.recordAndCheck('get', 'highWaterMark', side)) {
+        throw new Error(`intentional failure of get highWaterMark`);
+      }
+      return createRecordingNumberObject(record, 'highWaterMark', side);
+    }
+  };
+}
+
+// Creates an object which will record when it is converted to a number. It will assert if the conversion is to some
+// other type, and will fail if op('tonumber', property, side) is set as the failure step. The object will convert to -1
+// if 'validate' is set as the failure step, and 1 otherwise.
+function createRecordingNumberObject(record, property, side = undefined) {
+  return {
+    [Symbol.toPrimitive](hint) {
+      assert_equals(hint, 'number', `hint for ${property} should be 'number'`);
+      if (record.recordAndCheck('tonumber', property, side)) {
+        throw new Error(`intentional failure of ${op('tonumber', property, side)}`);
+      }
+      return record.check(property, side) ? -1 : 1;
+    }
+  };
+}
+
+// Creates a string from everything in |operations| up to and including |failureOp|. "validate" steps are excluded from
+// the output, as we cannot record them except by making them fail.
+function expectedAsString(operations, failureOp) {
+  const expected = [];
+  for (const step of operations) {
+    if (step.type !== 'validate') {
+      expected.push(step);
+    }
+    if (step.equals(failureOp)) {
+      break;
+    }
+  }
+  return expected.toString();
+}
diff --git a/streams/transform-streams/constructor.dedicatedworker.html b/streams/transform-streams/constructor.dedicatedworker.html
new file mode 100644
index 0000000..aebe97e
--- /dev/null
+++ b/streams/transform-streams/constructor.dedicatedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js dedicated worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new Worker('constructor.js'));
+</script>
diff --git a/streams/transform-streams/constructor.html b/streams/transform-streams/constructor.html
new file mode 100644
index 0000000..a548e08
--- /dev/null
+++ b/streams/transform-streams/constructor.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js browser context wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="../resources/constructor-ordering.js"></script>
+
+<script src="constructor.js"></script>
diff --git a/streams/transform-streams/constructor.js b/streams/transform-streams/constructor.js
new file mode 100644
index 0000000..6277003
--- /dev/null
+++ b/streams/transform-streams/constructor.js
@@ -0,0 +1,51 @@
+'use strict';
+
+if (self.importScripts) {
+  self.importScripts('/resources/testharness.js');
+  self.importScripts('../resources/constructor-ordering.js');
+}
+
+const operations = [
+  op('get', 'size', 'writable'),
+  op('get', 'highWaterMark', 'writable'),
+  op('get', 'size', 'readable'),
+  op('get', 'highWaterMark', 'readable'),
+  op('get', 'writableType'),
+  op('validate', 'writableType'),
+  op('validate', 'size', 'writable'),
+  op('tonumber', 'highWaterMark', 'writable'),
+  op('validate', 'highWaterMark', 'writable'),
+  op('get', 'readableType'),
+  op('validate', 'readableType'),
+  op('validate', 'size', 'readable'),
+  op('tonumber', 'highWaterMark', 'readable'),
+  op('validate', 'highWaterMark', 'readable'),
+  op('get', 'transform'),
+  op('validate', 'transform'),
+  op('get', 'flush'),
+  op('validate', 'flush'),
+  op('get', 'start'),
+  op('validate', 'start')
+];
+
+for (const failureOp of operations) {
+  test(() => {
+    const record = new OpRecorder(failureOp);
+    const transformer = createRecordingObjectWithProperties(
+        record, ['readableType', 'writableType', 'start', 'transform', 'flush']);
+    const writableStrategy = createRecordingStrategy(record, 'writable');
+    const readableStrategy = createRecordingStrategy(record, 'readable');
+
+    try {
+      new TransformStream(transformer, writableStrategy, readableStrategy);
+      assert_unreached('constructor should throw');
+    } catch (e) {
+      assert_equals(typeof e, 'object', 'e should be an object');
+    }
+
+    assert_equals(record.actual(), expectedAsString(operations, failureOp),
+                  'operations should be performed in the right order');
+  }, `TransformStream constructor should stop after ${failureOp} fails`);
+}
+
+done();
diff --git a/streams/transform-streams/constructor.serviceworker.https.html b/streams/transform-streams/constructor.serviceworker.https.html
new file mode 100644
index 0000000..ddddf06
--- /dev/null
+++ b/streams/transform-streams/constructor.serviceworker.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js service worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+'use strict';
+service_worker_test('constructor.js', 'Service worker test setup');
+</script>
diff --git a/streams/transform-streams/constructor.sharedworker.html b/streams/transform-streams/constructor.sharedworker.html
new file mode 100644
index 0000000..52b1a5d
--- /dev/null
+++ b/streams/transform-streams/constructor.sharedworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>constructor.js shared worker wrapper file</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+'use strict';
+fetch_tests_from_worker(new SharedWorker('constructor.js'));
+</script>
diff --git a/streams/transform-streams/properties.js b/streams/transform-streams/properties.js
index 7aefa9f..f8d8fac 100644
--- a/streams/transform-streams/properties.js
+++ b/streams/transform-streams/properties.js
@@ -182,10 +182,10 @@
     }
     const transformer = new Proxy({}, handler);
     const ts = new TransformStream(transformer, undefined, { highWaterMark: Infinity });
-    assert_array_equals(touchedProperties, ['readableType', 'writableType', 'transform', 'flush', 'start'],
+    assert_array_equals(touchedProperties, ['writableType', 'readableType', 'transform', 'flush', 'start'],
                         'expected properties should be got');
     return trigger(ts).then(() => {
-      assert_array_equals(touchedProperties, ['readableType', 'writableType', 'transform', 'flush', 'start'],
+      assert_array_equals(touchedProperties, ['writableType', 'readableType', 'transform', 'flush', 'start'],
                           'no properties should be accessed on method call');
     });
   }, `unexpected properties should not be accessed when calling transformer method ${method}`);
diff --git a/streams/writable-streams/constructor.html b/streams/writable-streams/constructor.html
index ec5a0fa..a548e08 100644
--- a/streams/writable-streams/constructor.html
+++ b/streams/writable-streams/constructor.html
@@ -5,6 +5,6 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 
-
+<script src="../resources/constructor-ordering.js"></script>
 
 <script src="constructor.js"></script>
diff --git a/streams/writable-streams/constructor.js b/streams/writable-streams/constructor.js
index a2d986c..5f28c8b 100644
--- a/streams/writable-streams/constructor.js
+++ b/streams/writable-streams/constructor.js
@@ -2,6 +2,7 @@
 
 if (self.importScripts) {
   self.importScripts('/resources/testharness.js');
+  self.importScripts('../resources/constructor-ordering.js');
 }
 
 const error1 = new Error('error1');
@@ -119,7 +120,7 @@
 
   assert_throws(new TypeError(), () => new WritableStreamDefaultController({}),
                 'constructor should throw a TypeError exception');
-}, 'WritableStreamDefaultController constructor should throw unless passed a WritableStream');
+}, 'WritableStreamDefaultController constructor should throw');
 
 test(() => {
   let WritableStreamDefaultController;
@@ -150,4 +151,40 @@
                 'constructor should throw a TypeError exception');
 }, 'WritableStreamDefaultWriter constructor should throw when stream argument is locked');
 
+const operations = [
+  op('get', 'size'),
+  op('get', 'highWaterMark'),
+  op('get', 'type'),
+  op('validate', 'type'),
+  op('validate', 'size'),
+  op('tonumber', 'highWaterMark'),
+  op('validate', 'highWaterMark'),
+  op('get', 'write'),
+  op('validate', 'write'),
+  op('get', 'close'),
+  op('validate', 'close'),
+  op('get', 'abort'),
+  op('validate', 'abort'),
+  op('get', 'start'),
+  op('validate', 'start')
+];
+
+for (const failureOp of operations) {
+  test(() => {
+    const record = new OpRecorder(failureOp);
+    const underlyingSink = createRecordingObjectWithProperties(record, ['type', 'start', 'write', 'close', 'abort']);
+    const strategy = createRecordingStrategy(record);
+
+    try {
+      new WritableStream(underlyingSink, strategy);
+      assert_unreached('constructor should throw');
+    } catch (e) {
+      assert_equals(typeof e, 'object', 'e should be an object');
+    }
+
+    assert_equals(record.actual(), expectedAsString(operations, failureOp),
+                  'operations should be performed in the right order');
+  }, `WritableStream constructor should stop after ${failureOp} fails`);
+}
+
 done();
diff --git a/tools/certs/README.md b/tools/certs/README.md
new file mode 100644
index 0000000..f5ae741
--- /dev/null
+++ b/tools/certs/README.md
@@ -0,0 +1,7 @@
+To enable https://web-platform.test:8443/, add cacert.pem to your browser as Certificate Authority.
+
+For Firefox, go to about:preferences and search for "certificates".
+
+For browsers that use the Certificate Authorities of the underlying OS, such as Chrome and Safari,
+you need to adjust the OS. For macOS, go to Keychain Access and add the certificate under
+**login**.
diff --git a/tools/certs/cakey.pem b/tools/certs/cacert.key
similarity index 100%
rename from tools/certs/cakey.pem
rename to tools/certs/cacert.key
diff --git a/tools/ci/ci_taskcluster.sh b/tools/ci/ci_taskcluster.sh
new file mode 100755
index 0000000..00e1f0b
--- /dev/null
+++ b/tools/ci/ci_taskcluster.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+./wpt manifest-download
+if [ $1 == "firefox" ]; then
+    ./wpt run firefox --log-tbpl=../artifacts/log_tbpl.log --log-tbpl-level=info --log-wptreport=../artifacts/wpt_report.json --log-mach=- --this-chunk=$3 --total-chunks=$4 --test-type=$2 -y --install-browser --no-pause --no-restart-on-unexpected --reftest-internal
+elif [ $1 == "chrome" ]; then
+    ./wpt run chrome --log-tbpl==../artifacts/log_tbpl.log --log-tbpl-level=info --log-wptreport=../artifacts/wpt_report.json --log-mach=- --this-chunk=$3 --total-chunks=$4 --test-type=$2 -y  --no-pause --no-restart-on-unexpected
+fi
diff --git a/tools/ci/commands.json b/tools/ci/commands.json
index a8db428..51dbbd2 100644
--- a/tools/ci/commands.json
+++ b/tools/ci/commands.json
@@ -3,5 +3,7 @@
                   "virtualenv": false},
     "check-stability": {"path": "check_stability.py", "script": "run", "parser": "get_parser", "parse_known": true, "help": "Check test stability",
                         "virtualenv": true, "install": ["requests"], "requirements": ["../wptrunner/requirements.txt"]},
-  "make-hosts-file": {"path": "make_hosts_file.py", "script": "run", "parser": "create_parser", "help": "Output a hosts file to stdout", "virtualenv": false}
+    "make-hosts-file": {"path": "make_hosts_file.py", "script": "run", "parser": "create_parser", "help": "Output a hosts file to stdout", "virtualenv": false},
+    "make-tasks": {"path": "taskgraph.py", "script": "run", "parser": "get_parser", "parse_known": true, "help": "Generate taskcluster.yml file containing the run tasks",
+                        "virtualenv": true, "install": ["pyyaml"]}
 }
diff --git a/tools/ci/tag_master.py b/tools/ci/tag_master.py
index 4404a19..f049671 100644
--- a/tools/ci/tag_master.py
+++ b/tools/ci/tag_master.py
@@ -13,35 +13,38 @@
 
 from tools.wpt.testfiles import get_git_cmd
 
-logging.basicConfig()
+logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
 
-def get_pr(repo, owner, rev):
-    url = ("https://api.github.com/search/issues?q=type:pr+is:merged+repo:%s/%s+%s" %
-           (repo, owner, rev))
+def get_pr(repo, owner, sha):
+    url = ("https://api.github.com/search/issues?q=type:pr+is:merged+repo:%s/%s+sha:%s" %
+           (repo, owner, sha))
     try:
         resp = urllib2.urlopen(url)
+        body = resp.read()
     except Exception as e:
         logger.error(e)
         return None
 
     if resp.code != 200:
-        logger.error("Got HTTP status %s" % resp.code)
+        logger.error("Got HTTP status %s. Response:" % resp.code)
+        logger.error(body)
         return None
 
     try:
-        data = json.loads(resp.read())
+        data = json.loads(body)
     except ValueError:
-        logger.error("Failed to read response as JSON")
+        logger.error("Failed to read response as JSON:")
+        logger.error(body)
         return None
 
     items = data["items"]
     if len(items) == 0:
-        logger.error("No PR found for master")
+        logger.error("No PR found for %s" % sha)
         return None
     if len(items) > 1:
-        logger.warning("Found multiple PRs for master")
+        logger.warning("Found multiple PRs for %s" % sha)
 
     pr = items[0]
 
@@ -63,12 +66,15 @@
         resp = opener.open(req)
     except Exception as e:
         logger.error("Tag creation failed:\n%s" % e)
-        return
+        return False
 
     if resp.code != 201:
-        logger.error("Got HTTP status %s" % resp.code)
-    else:
-        logger.info("Tagged master as %s" % tag)
+        logger.error("Got HTTP status %s. Response:" % resp.code)
+        logger.error(resp.read())
+        return False
+
+    logger.info("Tagged %s as %s" % (sha, tag))
+    return True
 
 
 def main():
@@ -84,8 +90,11 @@
     head_rev = git("rev-parse", "HEAD")
 
     pr = get_pr(owner, repo, head_rev)
-    if pr is not None:
-        tag(owner, repo, head_rev, "merge_pr_%s" % pr)
+    if pr is None:
+        sys.exit(1)
+    tagged = tag(owner, repo, head_rev, "merge_pr_%s" % pr)
+    if not tagged:
+        sys.exit(1)
 
 
 if __name__ == "__main__":
diff --git a/tools/ci/taskgraph.py b/tools/ci/taskgraph.py
new file mode 100644
index 0000000..2fcdba0
--- /dev/null
+++ b/tools/ci/taskgraph.py
@@ -0,0 +1,123 @@
+import argparse
+import copy
+import os
+import six
+
+import yaml
+
+
+here = os.path.dirname(__file__)
+wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir))
+
+
+task_template = {
+    "provisionerId": "{{ taskcluster.docker.provisionerId }}",
+    "workerType": "{{ taskcluster.docker.workerType }}",
+    "extra": {
+        "github": {
+            "events": ["push"],
+            "branches": ["master"],
+        },
+    },
+    "payload": {
+        "maxRunTime": 7200,
+        "image": "harjgam/web-platform-tests:0.6",
+        "command":[
+            "/bin/bash",
+            "--login",
+            "-c",
+            """>-
+            ~/start.sh &&
+            cd /home/test/web-platform-tests &&
+            git fetch {{event.head.repo.url}} &&
+            git config advice.detachedHead false &&
+            git checkout {{event.head.sha}} &&
+            %(command)s"""],
+        "artifacts": {
+            "public/results": {
+                "path": "/home/test/artifacts",
+                "type": "directory"
+            }
+        }
+    },
+    "metadata": {
+        "name": "wpt-%(browser_name)s-%(suite)s-%(chunk)s",
+        "description": "",
+        "owner": "{{ event.head.user.email }}",
+        "source": "{{ event.head.repo.url }}",
+    }
+}
+
+
+file_template = {
+    "version": 0,
+    "tasks": [],
+    "allowPullRequests": "collaborators"
+}
+
+suites = {
+    "testharness": {"chunks": 15},
+    "reftest": {"chunks": 10},
+    "wdspec": {"chunks": 1}
+}
+
+browsers = {
+    "firefox": {"name": "firefox-nightly"},
+    "chrome": {"name": "chrome-dev"}
+}
+
+
+def get_parser():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--dest",
+                        action="store",
+                        default=wpt_root,
+                        help="Directory to write the .taskcluster.yml file to")
+    return parser
+
+
+def fill(template, data):
+    rv = {}
+    for key, value in template.iteritems():
+        rv[key] = fill_inner(value, data)
+    return rv
+
+
+def fill_inner(value, data):
+    if isinstance(value, six.string_types):
+        return value % data
+    elif isinstance(value, dict):
+        return fill(value, data)
+    elif isinstance(value, list):
+        return [fill_inner(item, data) for item in value]
+    elif isinstance(value, (float,) + six.integer_types):
+        return value
+    else:
+        raise ValueError
+
+
+def run(venv, *args, **kwargs):
+    if not os.path.isdir(kwargs["dest"]):
+        raise ValueError("Invalid directory %s" % kwargs["dest"])
+
+    task_config = copy.deepcopy(file_template)
+    for browser, browser_props in browsers.iteritems():
+        for suite, suite_props in suites.iteritems():
+            total_chunks = suite_props.get("chunks", 1)
+            for chunk in six.moves.xrange(1, total_chunks + 1):
+                data = {
+                    "suite": suite,
+                    "chunk": chunk,
+                    "browser_name": browser_props["name"],
+                    "command": ("./tools/ci/ci_taskcluster.sh %s %s %s %s" %
+                                (browser, suite, chunk, total_chunks))
+                }
+
+                task_data = fill(task_template, data)
+                task_config["tasks"].append(task_data)
+
+    with open(os.path.join(kwargs["dest"], ".taskcluster.yml"), "w") as f:
+        f.write("""# GENERATED FILE DO NOT EDIT
+# To regenerate this file run ./wpt make-tasks
+""")
+        yaml.dump(task_config, f)
diff --git a/tools/docker/.bashrc b/tools/docker/.bashrc
new file mode 100644
index 0000000..bbe03c4
--- /dev/null
+++ b/tools/docker/.bashrc
@@ -0,0 +1,4 @@
+function xvfb_start() {
+    GEOMETRY="$SCREEN_WIDTH""x""$SCREEN_HEIGHT""x""$SCREEN_DEPTH"
+    xvfb-run --server-args="-screen 0 $GEOMETRY -ac +extension RANDR" $@
+}
diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile
new file mode 100644
index 0000000..7157ee3
--- /dev/null
+++ b/tools/docker/Dockerfile
@@ -0,0 +1,76 @@
+FROM ubuntu:16.04
+
+# No interactive frontend during docker build
+ENV DEBIAN_FRONTEND=noninteractive \
+    DEBCONF_NONINTERACTIVE_SEEN=true
+
+# General requirements not in the base image
+RUN apt-get -qqy update \
+  && apt-get -qqy install \
+    bzip2 \
+    ca-certificates \
+    dbus-x11 \
+    gdebi \
+    git \
+    locales \
+    pulseaudio \
+    python \
+    python-pip \
+    tzdata \
+    sudo \
+    unzip \
+    wget \
+    xvfb
+
+# Installing just the deps of firefox and chrome is moderately tricky, so
+# just install the default versions of them, and some extra deps we happen
+# to know that chrome requires
+
+RUN apt-get -qqy install \
+    firefox \
+    libnss3-tools \
+    fonts-liberation \
+    indicator-application \
+    libappindicator1 \
+    libappindicator3-1 \
+    libdbusmenu-gtk3-4 \
+    libindicator3-7 \
+    libindicator7
+
+RUN pip install --upgrade pip
+RUN pip install virtualenv
+
+ENV TZ "UTC"
+RUN echo "${TZ}" > /etc/timezone \
+  && dpkg-reconfigure --frontend noninteractive tzdata
+
+RUN useradd test \
+         --shell /bin/bash  \
+         --create-home \
+  && usermod -a -G sudo test \
+  && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \
+  && echo 'test:secret' | chpasswd
+
+ENV SCREEN_WIDTH 1280
+ENV SCREEN_HEIGHT 1024
+ENV SCREEN_DEPTH 24
+ENV DISPLAY :99.0
+
+USER test
+
+WORKDIR /home/test
+
+COPY .bashrc /home/test/.bashrc
+
+COPY start.sh /home/test/start.sh
+
+# Remove information on how to use sudo on login
+RUN sudo echo ""
+
+RUN git clone --depth=1 https://github.com/w3c/web-platform-tests.git
+
+RUN mkdir -p /home/test/.fonts && \
+    cp web-platform-tests/fonts/Ahem.ttf ~/.fonts && \
+    fc-cache -f -v
+
+RUN mkdir -p /home/test/artifacts
diff --git a/tools/docker/start.sh b/tools/docker/start.sh
new file mode 100755
index 0000000..908ec0c
--- /dev/null
+++ b/tools/docker/start.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+cd web-platform-tests
+git pull --depth=50
+
+sudo sh -c './wpt make-hosts-file >> /etc/hosts'
+
+# Install Chome dev
+deb_archive=google-chrome-unstable_current_amd64.deb
+wget https://dl.google.com/linux/direct/$deb_archive
+
+sudo gdebi -n $deb_archive
+
+sudo Xvfb $DISPLAY -screen 0 ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH} &
diff --git a/tools/lint/lint.py b/tools/lint/lint.py
index 8c53d21..a212994 100644
--- a/tools/lint/lint.py
+++ b/tools/lint/lint.py
@@ -18,7 +18,7 @@
 from ..gitignore.gitignore import PathFilter
 from ..wpt import testfiles
 
-from manifest.sourcefile import SourceFile, js_meta_re, python_meta_re, space_chars
+from manifest.sourcefile import SourceFile, js_meta_re, python_meta_re, space_chars, get_any_variants, get_default_any_variants
 from six import binary_type, iteritems, itervalues
 from six.moves import range
 from six.moves.urllib.parse import urlsplit, urljoin
@@ -32,7 +32,10 @@
     if logger is None:
         logger = logging.getLogger(os.path.basename(os.path.splitext(__file__)[0]))
         handler = logging.StreamHandler(sys.stdout)
-        logger.addHandler(handler)
+        # Only add a handler if the parent logger is missing a handler
+        if logger.parent and len(logger.parent.handlers) == 0:
+            handler = logging.StreamHandler(sys.stdout)
+            logger.addHandler(handler)
     if prefix:
         format = logging.BASIC_FORMAT
     else:
@@ -613,6 +616,31 @@
 broken_python_metadata = re.compile(b"#\s*META:")
 
 
+def check_global_metadata(value):
+    global_values = {item.strip() for item in value.split(b",") if item.strip()}
+
+    included_variants = set.union(get_default_any_variants(),
+                                  *(get_any_variants(v) for v in global_values if not v.startswith(b"!")))
+
+    for global_value in global_values:
+        if global_value.startswith(b"!"):
+            excluded_value = global_value[1:]
+            if not get_any_variants(excluded_value):
+                yield ("UNKNOWN-GLOBAL-METADATA", "Unexpected value for global metadata")
+
+            elif excluded_value in global_values:
+                yield ("BROKEN-GLOBAL-METADATA", "Cannot specify both %s and %s" % (global_value, excluded_value))
+
+            else:
+                excluded_variants = get_any_variants(excluded_value)
+                if not (excluded_variants & included_variants):
+                    yield ("BROKEN-GLOBAL-METADATA", "Cannot exclude %s if it is not included" % (excluded_value,))
+
+        else:
+            if not get_any_variants(global_value):
+                yield ("UNKNOWN-GLOBAL-METADATA", "Unexpected value for global metadata")
+
+
 def check_script_metadata(repo_root, path, f):
     if path.endswith((".worker.js", ".any.js")):
         meta_re = js_meta_re
@@ -631,7 +659,9 @@
         m = meta_re.match(line)
         if m:
             key, value = m.groups()
-            if key == b"timeout":
+            if key == b"global":
+                errors.extend((kind, message, path, idx + 1) for (kind, message) in check_global_metadata(value))
+            elif key == b"timeout":
                 if value != b"long":
                     errors.append(("UNKNOWN-TIMEOUT-METADATA", "Unexpected value for timeout metadata", path, idx + 1))
             elif key == b"script":
@@ -783,8 +813,6 @@
                         help="Output machine-readable JSON format")
     parser.add_argument("--markdown", action="store_true",
                         help="Output markdown")
-    parser.add_argument("--css-mode", action="store_true",
-                        help="Run CSS testsuite specific lints")
     parser.add_argument("--repo-root", help="The WPT directory. Use this"
                         "option if the lint script exists outside the repository")
     parser.add_argument("--all", action="store_true", help="If no paths are passed, try to lint the whole "
diff --git a/tools/lint/tests/test_file_lints.py b/tools/lint/tests/test_file_lints.py
index f4fb221..b42a2dc 100644
--- a/tools/lint/tests/test_file_lints.py
+++ b/tools/lint/tests/test_file_lints.py
@@ -559,6 +559,39 @@
         assert errors == []
 
 
+@pytest.mark.parametrize("globals,error", [
+    (b"", None),
+    (b"default", None),
+    (b"!default", None),
+    (b"window", None),
+    (b"!window", None),
+    (b"!dedicatedworker", None),
+    (b"window, !window", "BROKEN-GLOBAL-METADATA"),
+    (b"!serviceworker", "BROKEN-GLOBAL-METADATA"),
+    (b"serviceworker, !serviceworker", "BROKEN-GLOBAL-METADATA"),
+    (b"worker, !dedicatedworker", None),
+    (b"worker, !serviceworker", None),
+    (b"!worker", None),
+    (b"foo", "UNKNOWN-GLOBAL-METADATA"),
+    (b"!foo", "UNKNOWN-GLOBAL-METADATA"),
+])
+def test_script_globals_metadata(globals, error):
+    filename = "foo.any.js"
+    input = b"""// META: global=%s\n""" % globals
+    errors = check_file_contents("", filename, six.BytesIO(input))
+    check_errors(errors)
+
+    if error is not None:
+        errors = [(k, f, l) for (k, _, f, l) in errors]
+        assert errors == [
+            (error,
+             filename,
+             1),
+        ]
+    else:
+        assert errors == []
+
+
 @pytest.mark.parametrize("input,error", [
     (b"""#META: timeout=long\n""", None),
     (b"""# META: timeout=long\n""", None),
diff --git a/tools/manifest/manifest.py b/tools/manifest/manifest.py
index 4a8deb0..815ce9d 100644
--- a/tools/manifest/manifest.py
+++ b/tools/manifest/manifest.py
@@ -1,6 +1,6 @@
+import itertools
 import json
 import os
-import re
 from collections import defaultdict
 from six import iteritems, itervalues, viewkeys, string_types
 
@@ -56,7 +56,8 @@
     def reftest_nodes_by_url(self):
         if self._reftest_nodes_by_url is None:
             by_url = {}
-            for path, nodes in iteritems(self._data.get("reftests", {})):
+            for path, nodes in itertools.chain(iteritems(self._data.get("reftest", {})),
+                                               iteritems(self._data.get("reftest_node", {}))):
                 for node in nodes:
                     by_url[node.url] = node
             self._reftest_nodes_by_url = by_url
@@ -143,13 +144,13 @@
                     changed_hashes[item.source_file.rel_path] = (item.source_file.hash,
                                                                  item.item_type)
                 references[item.source_file.rel_path].add(item)
-                self._reftest_nodes_by_url[item.url] = item
             else:
                 if isinstance(item, RefTestNode):
                     item = item.to_RefTest()
                     changed_hashes[item.source_file.rel_path] = (item.source_file.hash,
                                                                  item.item_type)
                 reftests[item.source_file.rel_path].add(item)
+            self._reftest_nodes_by_url[item.url] = item
 
         return reftests, references, changed_hashes
 
diff --git a/tools/manifest/sourcefile.py b/tools/manifest/sourcefile.py
index 093fc2b..0bf7e8b 100644
--- a/tools/manifest/sourcefile.py
+++ b/tools/manifest/sourcefile.py
@@ -49,6 +49,87 @@
         yield (m.groups()[0], m.groups()[1])
 
 
+_any_variants = {
+    b"default": {"longhand": {b"window", b"dedicatedworker"}},
+    b"window": {"suffix": ".any.html"},
+    b"serviceworker": {"force_https": True},
+    b"sharedworker": {},
+    b"dedicatedworker": {"suffix": ".any.worker.html"},
+    b"worker": {"longhand": {b"dedicatedworker", b"sharedworker", b"serviceworker"}}
+}
+
+
+def get_any_variants(item):
+    """
+    Returns a set of variants (bytestrings) defined by the given keyword.
+    """
+    assert isinstance(item, binary_type), item
+    assert not item.startswith(b"!"), item
+
+    variant = _any_variants.get(item, None)
+    if variant is None:
+        return set()
+
+    return variant.get("longhand", {item})
+
+
+def get_default_any_variants():
+    """
+    Returns a set of variants (bytestrings) that will be used by default.
+    """
+    return set(_any_variants[b"default"]["longhand"])
+
+
+def parse_variants(value):
+    """
+    Returns a set of variants (bytestrings) defined by a comma-separated value.
+    """
+    assert isinstance(value, binary_type), value
+
+    globals = get_default_any_variants()
+
+    for item in value.split(b","):
+        item = item.strip()
+        if item.startswith(b"!"):
+            globals -= get_any_variants(item[1:])
+        else:
+            globals |= get_any_variants(item)
+
+    return globals
+
+
+def global_suffixes(value):
+    """
+    Yields the relevant filename suffixes (strings) for the variants defined by
+    the given comma-separated value.
+    """
+    assert isinstance(value, binary_type), value
+
+    rv = set()
+
+    global_types = parse_variants(value)
+    for global_type in global_types:
+        variant = _any_variants[global_type]
+        suffix = variant.get("suffix", ".any.%s.html" % global_type.decode("utf-8"))
+        if variant.get("force_https", False):
+            suffix = ".https" + suffix
+        rv.add(suffix)
+
+    return rv
+
+
+def global_variant_url(url, suffix):
+    """
+    Returns a url created from the given url and suffix (all strings).
+    """
+    url = url.replace(".any.", ".")
+    # If the url must be loaded over https, ensure that it will have
+    # the form .https.any.js
+    if ".https." in url and suffix.startswith(".https."):
+        url = url.replace(".https.", ".")
+    return replace_end(url, ".js", suffix)
+
+
 class SourceFile(object):
     parsers = {"html":lambda x:html5lib.parse(x, treebuilder="etree"),
                "xhtml":lambda x:ElementTree.parse(x, XMLParser.XMLParser()),
@@ -225,8 +306,9 @@
         # wdspec tests are in subdirectories of /webdriver excluding __init__.py
         # files.
         rel_dir_tree = self.rel_path.split(os.path.sep)
-        return (rel_dir_tree[0] == "webdriver" and
-                len(rel_dir_tree) > 1 and
+        return (((rel_dir_tree[0] == "webdriver" and len(rel_dir_tree) > 1) or
+                 (rel_dir_tree[:2] == ["infrastructure", "webdriver"] and
+                  len(rel_dir_tree) > 2)) and
                 self.filename not in ("__init__.py", "conftest.py") and
                 fnmatch(self.filename, wd_pattern))
 
@@ -369,11 +451,18 @@
     @cached_property
     def test_variants(self):
         rv = []
-        for element in self.variant_nodes:
-            if "content" in element.attrib:
-                variant = element.attrib["content"]
-                assert variant == "" or variant[0] in ["#", "?"]
-                rv.append(variant)
+        if self.ext == ".js":
+            for (key, value) in self.script_metadata:
+                if key == b"variant":
+                    rv.append(value.decode("utf-8"))
+        else:
+            for element in self.variant_nodes:
+                if "content" in element.attrib:
+                    variant = element.attrib["content"]
+                    rv.append(variant)
+
+        for variant in rv:
+            assert variant == "" or variant[0] in ["#", "?"], variant
 
         if not rv:
             rv = [""]
@@ -471,7 +560,7 @@
     @cached_property
     def content_is_css_visual(self):
         """Boolean indicating whether the file content represents a
-        CSS WG-style manual test"""
+        CSS WG-style visual test"""
         if self.root is None:
             return None
         return bool(self.ext in {'.xht', '.html', '.xhtml', '.htm', '.xml', '.svg'} and
@@ -509,22 +598,34 @@
             rv = VisualTest.item_type, [VisualTest(self, self.url)]
 
         elif self.name_is_multi_global:
-            rv = TestharnessTest.item_type, [
-                TestharnessTest(self, replace_end(self.url, ".any.js", ".any.html"),
-                                timeout=self.timeout),
-                TestharnessTest(self, replace_end(self.url, ".any.js", ".any.worker.html"),
-                                timeout=self.timeout),
+            globals = b""
+            for (key, value) in self.script_metadata:
+                if key == b"global":
+                    globals = value
+                    break
+
+            tests = [
+                TestharnessTest(self, global_variant_url(self.url, suffix) + variant, timeout=self.timeout)
+                for suffix in sorted(global_suffixes(globals))
+                for variant in self.test_variants
             ]
+            rv = TestharnessTest.item_type, tests
 
         elif self.name_is_worker:
-            rv = (TestharnessTest.item_type,
-                  [TestharnessTest(self, replace_end(self.url, ".worker.js", ".worker.html"),
-                                   timeout=self.timeout)])
+            test_url = replace_end(self.url, ".worker.js", ".worker.html")
+            tests = [
+                TestharnessTest(self, test_url + variant, timeout=self.timeout)
+                for variant in self.test_variants
+            ]
+            rv = TestharnessTest.item_type, tests
 
         elif self.name_is_window:
-            rv = (TestharnessTest.item_type,
-                  [TestharnessTest(self, replace_end(self.url, ".window.js", ".window.html"),
-                                   timeout=self.timeout)])
+            test_url = replace_end(self.url, ".window.js", ".window.html")
+            tests = [
+                TestharnessTest(self, test_url + variant, timeout=self.timeout)
+                for variant in self.test_variants
+            ]
+            rv = TestharnessTest.item_type, tests
 
         elif self.name_is_webdriver:
             rv = WebdriverSpecTest.item_type, [WebdriverSpecTest(self, self.url,
diff --git a/tools/manifest/tests/test_manifest.py b/tools/manifest/tests/test_manifest.py
index 075d689..770433d 100644
--- a/tools/manifest/tests/test_manifest.py
+++ b/tools/manifest/tests/test_manifest.py
@@ -292,3 +292,22 @@
                                                                  "/test2-1.html",
                                                                  "/test2-2.html"])
     assert set(m.iterpath("missing")) == set()
+
+
+def test_reftest_node_by_url():
+    m = manifest.Manifest()
+
+    s1 = SourceFileWithTest("test1", "0"*40, item.RefTest, [("/test2", "==")])
+    s2 = SourceFileWithTest("test2", "0"*40, item.RefTest, [("/test3", "==")])
+
+    m.update([s1, s2])
+
+    test1 = s1.manifest_items()[1][0]
+    test2 = s2.manifest_items()[1][0]
+    test2_node = test2.to_RefTestNode()
+
+    assert m.reftest_nodes_by_url == {"/test1": test1,
+                                      "/test2": test2_node}
+    m._reftest_nodes_by_url = None
+    assert m.reftest_nodes_by_url == {"/test1": test1,
+                                      "/test2": test2_node}
diff --git a/tools/manifest/tests/test_sourcefile.py b/tools/manifest/tests/test_sourcefile.py
index 57863bf..6e10592 100644
--- a/tools/manifest/tests/test_sourcefile.py
+++ b/tools/manifest/tests/test_sourcefile.py
@@ -3,6 +3,7 @@
 import pytest
 
 from six import BytesIO
+from ...lint.lint import check_global_metadata
 from ..sourcefile import SourceFile, read_script_metadata, js_meta_re, python_meta_re
 
 def create(filename, contents=b""):
@@ -201,6 +202,66 @@
         assert item.timeout == "long"
 
 
+def test_worker_with_variants():
+    contents = b"""// META: variant=
+// META: variant=?wss
+test()"""
+
+    s = create("html/test.worker.js", contents=contents)
+    assert not s.name_is_non_test
+    assert not s.name_is_manual
+    assert not s.name_is_visual
+    assert not s.name_is_multi_global
+    assert s.name_is_worker
+    assert not s.name_is_window
+    assert not s.name_is_reference
+
+    assert not s.content_is_testharness
+
+    item_type, items = s.manifest_items()
+    assert item_type == "testharness"
+
+    expected_urls = [
+        "/html/test.worker.html" + suffix
+        for suffix in ["", "?wss"]
+    ]
+    assert len(items) == len(expected_urls)
+
+    for item, url in zip(items, expected_urls):
+        assert item.url == url
+        assert item.timeout is None
+
+
+def test_window_with_variants():
+    contents = b"""// META: variant=
+// META: variant=?wss
+test()"""
+
+    s = create("html/test.window.js", contents=contents)
+    assert not s.name_is_non_test
+    assert not s.name_is_manual
+    assert not s.name_is_visual
+    assert not s.name_is_multi_global
+    assert not s.name_is_worker
+    assert s.name_is_window
+    assert not s.name_is_reference
+
+    assert not s.content_is_testharness
+
+    item_type, items = s.manifest_items()
+    assert item_type == "testharness"
+
+    expected_urls = [
+        "/html/test.window.html" + suffix
+        for suffix in ["", "?wss"]
+    ]
+    assert len(items) == len(expected_urls)
+
+    for item, url in zip(items, expected_urls):
+        assert item.url == url
+        assert item.timeout is None
+
+
 def test_python_long_timeout():
     contents = b"""# META: timeout=long
 
@@ -264,6 +325,99 @@
 
 
 @pytest.mark.parametrize("input,expected", [
+    (b"", {"dedicatedworker", "window"}),
+    (b"default", {"dedicatedworker", "window"}),
+    (b"!default", {}),
+    (b"!default,window", {"window"}),
+    (b"window,!default", {}),
+    (b"!default,dedicatedworker", {"dedicatedworker"}),
+    (b"dedicatedworker,!default", {}),
+    (b"!default,worker", {"dedicatedworker", "serviceworker", "sharedworker"}),
+    (b"worker,!default", {"serviceworker", "sharedworker"}),
+    (b"!dedicatedworker", {"window"}),
+    (b"!worker", {"window"}),
+    (b"!window", {"dedicatedworker"}),
+    (b"!window,worker", {"dedicatedworker", "serviceworker", "sharedworker"}),
+    (b"worker,!dedicatedworker", {"serviceworker", "sharedworker", "window"}),
+    (b"!dedicatedworker,worker", {"dedicatedworker", "serviceworker", "sharedworker", "window"}),
+    (b"worker,!sharedworker", {"dedicatedworker", "serviceworker", "window"}),
+    (b"!sharedworker,worker", {"dedicatedworker", "serviceworker", "sharedworker", "window"}),
+    (b"sharedworker", {"dedicatedworker", "sharedworker", "window"}),
+    (b"sharedworker,serviceworker", {"dedicatedworker", "serviceworker", "sharedworker", "window"}),
+])
+def test_multi_global_with_custom_globals(input, expected):
+    contents = b"""// META: global=%s
+test()""" % input
+
+    assert list(check_global_metadata(input)) == []
+
+    s = create("html/test.any.js", contents=contents)
+    assert not s.name_is_non_test
+    assert not s.name_is_manual
+    assert not s.name_is_visual
+    assert s.name_is_multi_global
+    assert not s.name_is_worker
+    assert not s.name_is_reference
+
+    assert not s.content_is_testharness
+
+    item_type, items = s.manifest_items()
+    assert item_type == "testharness"
+
+    urls = {
+        "dedicatedworker": "/html/test.any.worker.html",
+        "serviceworker": "/html/test.https.any.serviceworker.html",
+        "sharedworker": "/html/test.any.sharedworker.html",
+        "window": "/html/test.any.html",
+    }
+
+    expected_urls = sorted(urls[ty] for ty in expected)
+    assert len(items) == len(expected_urls)
+
+    for item, url in zip(items, expected_urls):
+        assert item.url == url
+        assert item.timeout is None
+
+
+def test_multi_global_with_variants():
+    contents = b"""// META: global=window,worker
+// META: variant=
+// META: variant=?wss
+test()"""
+
+    s = create("html/test.any.js", contents=contents)
+    assert not s.name_is_non_test
+    assert not s.name_is_manual
+    assert not s.name_is_visual
+    assert s.name_is_multi_global
+    assert not s.name_is_worker
+    assert not s.name_is_reference
+
+    assert not s.content_is_testharness
+
+    item_type, items = s.manifest_items()
+    assert item_type == "testharness"
+
+    urls = {
+        "dedicatedworker": "/html/test.any.worker.html",
+        "serviceworker": "/html/test.https.any.serviceworker.html",
+        "sharedworker": "/html/test.any.sharedworker.html",
+        "window": "/html/test.any.html",
+    }
+
+    expected_urls = sorted(
+        urls[ty] + suffix
+        for ty in ["dedicatedworker", "serviceworker", "sharedworker", "window"]
+        for suffix in ["", "?wss"]
+    )
+    assert len(items) == len(expected_urls)
+
+    for item, url in zip(items, expected_urls):
+        assert item.url == url
+        assert item.timeout is None
+
+
+@pytest.mark.parametrize("input,expected", [
     (b"""//META: foo=bar\n""", [(b"foo", b"bar")]),
     (b"""// META: foo=bar\n""", [(b"foo", b"bar")]),
     (b"""//  META: foo=bar\n""", [(b"foo", b"bar")]),
@@ -395,7 +549,7 @@
     assert not s.name_is_worker
     assert not s.name_is_reference
 
-    assert s.root
+    assert s.root is not None
     assert s.content_is_testharness
 
     assert items(s) == [("testharness", "/" + filename)]
@@ -424,7 +578,7 @@
     assert not s.name_is_worker
     assert not s.name_is_reference
 
-    assert s.root
+    assert s.root is not None
     assert not s.content_is_testharness
 
     assert items(s) == []
diff --git a/tools/serve/serve.py b/tools/serve/serve.py
index fb6675d..1c56f16 100644
--- a/tools/serve/serve.py
+++ b/tools/serve/serve.py
@@ -5,8 +5,8 @@
 import abc
 import argparse
 import json
+import logging
 import os
-import re
 import socket
 import sys
 import threading
@@ -18,17 +18,18 @@
 from multiprocessing import Process, Event
 
 from localpaths import repo_root
+from six.moves import reload_module
 
-import sslutils
-from manifest.sourcefile import read_script_metadata, js_meta_re
+from manifest.sourcefile import read_script_metadata, js_meta_re, parse_variants
 from wptserve import server as wptserve, handlers
 from wptserve import stash
 from wptserve import config
 from wptserve.logger import set_logger
 from wptserve.handlers import filesystem_path, wrap_pipeline
-from wptserve.utils import get_port
+from wptserve.utils import get_port, HTTPException
 from mod_pywebsocket import standalone as pywebsocket
 
+
 def replace_end(s, old, new):
     """
     Given a string `s` that ends with `old`, replace that occurrence of `old`
@@ -56,9 +57,14 @@
         for header_name, header_value in self.headers:
             response.headers.set(header_name, header_value)
 
+        self.check_exposure(request)
+
         path = self._get_path(request.url_parts.path, True)
+        query = request.url_parts.query
+        if query:
+            query = "?" + query
         meta = "\n".join(self._get_meta(request))
-        response.content = self.wrapper % {"meta": meta, "path": path}
+        response.content = self.wrapper % {"meta": meta, "path": path, "query": query}
         wrap_pipeline(path, request, response)
 
     def _get_path(self, path, resource_path):
@@ -85,18 +91,27 @@
                 path = replace_end(path, src, dest)
         return path
 
-    def _get_meta(self, request):
-        """Get an iterator over strings to inject into the wrapper document
-        based on //META comments in the associated js file.
+    def _get_metadata(self, request):
+        """Get an iterator over script metadata based on //META comments in the
+        associated js file.
 
         :param request: The Request being processed.
         """
         path = self._get_path(filesystem_path(self.base_path, request, self.url_base), False)
         with open(path, "rb") as f:
             for key, value in read_script_metadata(f, js_meta_re):
-                replacement = self._meta_replacement(key, value)
-                if replacement:
-                    yield replacement
+                yield key, value
+
+    def _get_meta(self, request):
+        """Get an iterator over strings to inject into the wrapper document
+        based on //META comments in the associated js file.
+
+        :param request: The Request being processed.
+        """
+        for key, value in self._get_metadata(request):
+            replacement = self._meta_replacement(key, value)
+            if replacement:
+                yield replacement
 
     @abc.abstractproperty
     def path_replace(self):
@@ -118,8 +133,27 @@
         # a specific metadata key: value pair.
         pass
 
+    @abc.abstractmethod
+    def check_exposure(self, request):
+        # Raise an exception if this handler shouldn't be exposed after all.
+        pass
+
 
 class HtmlWrapperHandler(WrapperHandler):
+    global_type = None
+
+    def check_exposure(self, request):
+        if self.global_type:
+            globals = b""
+            for (key, value) in self._get_metadata(request):
+                if key == b"global":
+                    globals = value
+                    break
+
+            if self.global_type not in parse_variants(globals):
+                raise HTTPException(404, "This test cannot be loaded in %s mode" %
+                                    self.global_type)
+
     def _meta_replacement(self, key, value):
         if key == b"timeout":
             if value == b"long":
@@ -131,6 +165,7 @@
 
 
 class WorkersHandler(HtmlWrapperHandler):
+    global_type = b"dedicatedworker"
     path_replace = [(".any.worker.html", ".any.js", ".any.worker.js"),
                     (".worker.html", ".worker.js")]
     wrapper = """<!doctype html>
@@ -140,7 +175,7 @@
 <script src="/resources/testharnessreport.js"></script>
 <div id=log></div>
 <script>
-fetch_tests_from_worker(new Worker("%(path)s"));
+fetch_tests_from_worker(new Worker("%(path)s%(query)s"));
 </script>
 """
 
@@ -158,6 +193,7 @@
 
 
 class AnyHtmlHandler(HtmlWrapperHandler):
+    global_type = b"window"
     path_replace = [(".any.html", ".any.js")]
     wrapper = """<!doctype html>
 <meta charset=utf-8>
@@ -175,6 +211,42 @@
 """
 
 
+class SharedWorkersHandler(HtmlWrapperHandler):
+    global_type = b"sharedworker"
+    path_replace = [(".any.sharedworker.html", ".any.js", ".any.worker.js")]
+    wrapper = """<!doctype html>
+<meta charset=utf-8>
+%(meta)s
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+fetch_tests_from_worker(new SharedWorker("%(path)s%(query)s"));
+</script>
+"""
+
+
+class ServiceWorkersHandler(HtmlWrapperHandler):
+    global_type = b"serviceworker"
+    path_replace = [(".https.any.serviceworker.html", ".any.js", ".any.worker.js")]
+    wrapper = """<!doctype html>
+<meta charset=utf-8>
+%(meta)s
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+(async function() {
+  const scope = 'does/not/exist';
+  let reg = await navigator.serviceWorker.getRegistration(scope);
+  if (reg) await reg.unregister();
+  reg = await navigator.serviceWorker.register("%(path)s%(query)s", {scope});
+  fetch_tests_from_worker(reg.installing);
+})();
+</script>
+"""
+
+
 class AnyWorkerHandler(WrapperHandler):
     headers = [('Content-Type', 'text/javascript')]
     path_replace = [(".any.worker.js", ".any.js")]
@@ -189,8 +261,6 @@
 """
 
     def _meta_replacement(self, key, value):
-        if key == b"timeout":
-            return None
         if key == b"script":
             attribute = value.decode('utf-8').replace("\\", "\\\\").replace('"', '\\"')
             return 'importScripts("%s")' % attribute
@@ -243,6 +313,8 @@
             ("GET", "*.worker.html", WorkersHandler),
             ("GET", "*.window.html", WindowHandler),
             ("GET", "*.any.html", AnyHtmlHandler),
+            ("GET", "*.any.sharedworker.html", SharedWorkersHandler),
+            ("GET", "*.https.any.serviceworker.html", ServiceWorkersHandler),
             ("GET", "*.any.worker.js", AnyWorkerHandler),
             ("GET", "*.asis", handlers.AsIsHandler),
             ("*", "*.py", handlers.PythonScriptHandler),
@@ -327,9 +399,13 @@
         return self.proc.is_alive()
 
 
-def check_subdomains(domains, paths, bind_address, ssl_config, aliases):
-    domains = domains.copy()
-    host = domains.pop("")
+def check_subdomains(config):
+    paths = config.paths
+    bind_address = config.bind_address
+    ssl_config = config.ssl_config
+    aliases = config.aliases
+
+    host = config.server_host
     port = get_port(host)
     logger.debug("Going to use port %d to check subdomains" % port)
 
@@ -351,7 +427,10 @@
                         "You may need to edit /etc/hosts or similar, see README.md." % (host, port))
         sys.exit(1)
 
-    for domain in domains.itervalues():
+    for domain in config.domains_set:
+        if domain == host:
+            continue
+
         try:
             urllib2.urlopen("http://%s:%d/" % (domain, port))
         except Exception as e:
@@ -365,10 +444,10 @@
 def make_hosts_file(config, host):
     rv = []
 
-    for domain in config["domains"].values():
+    for domain in config.domains_set:
         rv.append("%s\t%s\n" % (host, domain))
 
-    for not_domain in config.get("not_domains", {}).values():
+    for not_domain in config.not_domains_set:
         rv.append("0.0.0.0\t%s\n" % not_domain)
 
     return "".join(rv)
@@ -494,6 +573,9 @@
 
 def start_ws_server(host, port, paths, routes, bind_address, config, ssl_config,
                     **kwargs):
+    # Ensure that when we start this in a new process we don't inherit the
+    # global lock in the logging module
+    reload_module(logging)
     return WebSocketDaemon(host,
                            str(port),
                            repo_root,
@@ -505,6 +587,9 @@
 
 def start_wss_server(host, port, paths, routes, bind_address, config, ssl_config,
                      **kwargs):
+    # Ensure that when we start this in a new process we don't inherit the
+    # global lock in the logging module
+    reload_module(logging)
     return WebSocketDaemon(host,
                            str(port),
                            repo_root,
@@ -617,9 +702,7 @@
     bind_address = config["bind_address"]
 
     if config["check_subdomains"]:
-        paths = config.paths
-        ssl_config = config.ssl_config
-        check_subdomains(config.domains, paths, bind_address, ssl_config, config["aliases"])
+        check_subdomains(config)
 
     stash_address = None
     if bind_address:
diff --git a/tools/serve/test_serve.py b/tools/serve/test_serve.py
index 3337601..6505cf3 100644
--- a/tools/serve/test_serve.py
+++ b/tools/serve/test_serve.py
@@ -1,12 +1,22 @@
 from . import serve
 
 def test_make_hosts_file():
-    hosts = serve.make_hosts_file({
-        "domains": {"www": "www.foo.bar.test", "www1": "www1.foo.bar.test"},
-        "not_domains": {"aaa": "aaa.foo.bar.test", "bbb": "bbb.foo.bar.test"}
-    }, "127.1.1.1")
+    c = serve.Config(browser_host="foo.bar", alternate_hosts={"alt": "foo2.bar"})
+    hosts = serve.make_hosts_file(c, "192.168.42.42")
     lines = hosts.split("\n")
-    assert "127.1.1.1\twww.foo.bar.test" in lines
-    assert "127.1.1.1\twww1.foo.bar.test" in lines
-    assert "0.0.0.0\taaa.foo.bar.test" in lines
-    assert "0.0.0.0\tbbb.foo.bar.test" in lines
+    assert set(lines) == {"",
+                          "0.0.0.0\tnonexistent-origin.foo.bar",
+                          "0.0.0.0\tnonexistent-origin.foo2.bar",
+                          "192.168.42.42\tfoo.bar",
+                          "192.168.42.42\tfoo2.bar",
+                          "192.168.42.42\twww.foo.bar",
+                          "192.168.42.42\twww.foo2.bar",
+                          "192.168.42.42\twww1.foo.bar",
+                          "192.168.42.42\twww1.foo2.bar",
+                          "192.168.42.42\twww2.foo.bar",
+                          "192.168.42.42\twww2.foo2.bar",
+                          "192.168.42.42\txn--lve-6lad.foo.bar",
+                          "192.168.42.42\txn--lve-6lad.foo2.bar",
+                          "192.168.42.42\txn--n8j6ds53lwwkrqhv28a.foo.bar",
+                          "192.168.42.42\txn--n8j6ds53lwwkrqhv28a.foo2.bar"}
+    assert lines[-1] == ""
diff --git a/tools/sslutils/openssl.py b/tools/sslutils/openssl.py
index 09c2471..f44d0de 100644
--- a/tools/sslutils/openssl.py
+++ b/tools/sslutils/openssl.py
@@ -136,7 +136,7 @@
 new_certs_dir = $certs
 crl_dir = $dir%(sep)scrl
 database = $dir%(sep)sindex.txt
-private_key = $dir%(sep)scakey.pem
+private_key = $dir%(sep)scacert.key
 certificate = $dir%(sep)scacert.pem
 serial = $dir%(sep)sserial
 crldir = $dir%(sep)scrl
@@ -294,7 +294,7 @@
         return self._ca_cert_path
 
     def _load_ca_cert(self):
-        key_path = self.path("cakey.pem")
+        key_path = self.path("cacert.key")
         cert_path = self.path("cacert.pem")
 
         if self.check_key_cert(key_path, cert_path, None):
@@ -327,7 +327,7 @@
         path = self.path
         self.logger.info("Generating new CA in %s" % self.base_path)
 
-        key_path = path("cakey.pem")
+        key_path = path("cacert.key")
         req_path = path("careq.pem")
         cert_path = path("cacert.pem")
 
diff --git a/tools/webdriver/webdriver/client.py b/tools/webdriver/webdriver/client.py
index 8da14a0..a8abda0 100644
--- a/tools/webdriver/webdriver/client.py
+++ b/tools/webdriver/webdriver/client.py
@@ -536,7 +536,12 @@
 
     @command
     def close(self):
-        return self.send_session_command("DELETE", "window")
+        handles = self.send_session_command("DELETE", "window")
+        if len(handles) == 0:
+            # With no more open top-level browsing contexts, the session is closed.
+            self.session_id = None
+
+        return handles
 
     @property
     @command
@@ -557,17 +562,23 @@
         return self.send_session_command("GET", url, {})
 
     @command
-    def set_cookie(self, name, value, path=None, domain=None, secure=None, expiry=None):
-        body = {"name": name,
-                "value": value}
-        if path is not None:
-            body["path"] = path
+    def set_cookie(self, name, value, path=None, domain=None,
+            secure=None, expiry=None, http_only=None):
+        body = {
+            "name": name,
+            "value": value,
+        }
+
         if domain is not None:
             body["domain"] = domain
-        if secure is not None:
-            body["secure"] = secure
         if expiry is not None:
             body["expiry"] = expiry
+        if http_only is not None:
+            body["httpOnly"] = http_only
+        if path is not None:
+            body["path"] = path
+        if secure is not None:
+            body["secure"] = secure
         self.send_session_command("POST", "cookie", {"cookie": body})
 
     def delete_cookie(self, name=None):
diff --git a/tools/wpt/browser.py b/tools/wpt/browser.py
index d52feb0..0c37e13 100644
--- a/tools/wpt/browser.py
+++ b/tools/wpt/browser.py
@@ -179,22 +179,70 @@
     def find_webdriver(self):
         return find_executable("geckodriver")
 
-    def install_prefs(self, dest=None):
+    def get_version_number(self, binary):
+        version_re = re.compile("Mozilla Firefox (\d+\.\d+(?:\.\d+)?)(a|b)?")
+        proc = subprocess.Popen([binary, "--version"], stdout=subprocess.PIPE)
+        stdout, _ = proc.communicate()
+        stdout.strip()
+        m = version_re.match(stdout)
+        if not m:
+            return None, "nightly"
+        version, status = m.groups()
+        channel = {"a": "nightly", "b": "beta"}
+        return version, channel.get(status, "stable")
+
+    def get_prefs_url(self, version, channel):
+        if channel == "stable":
+            repo = "https://hg.mozilla.org/releases/mozilla-release"
+            tag = "FIREFOX_%s_RELEASE" % version.replace(".", "_")
+        else:
+            repo = "https://hg.mozilla.org/mozilla-central"
+            if channel == "beta":
+                tag = "FIREFOX_%s_BETA" % version.split(".", 1)[0]
+            else:
+                # Always use tip as the tag for nightly; this isn't quite right
+                # but to do better we need the actual build revision, which we
+                # can get if we have an application.ini file
+                tag = "tip"
+
+        return "%s/raw-file/%s/testing/profiles/prefs_general.js" % (repo, tag)
+
+    def install_prefs(self, binary, dest=None):
+        version, channel = self.get_version_number(binary)
+
         if dest is None:
             dest = os.pwd
 
         dest = os.path.join(dest, "profiles")
         if not os.path.exists(dest):
             os.makedirs(dest)
-        prefs_path = os.path.join(dest, "prefs_general.js")
+        prefs_file = os.path.join(dest, "prefs_general.js")
+        cache_file = os.path.join(dest,
+                                  "%s-%s.cache" % (version, channel)
+                                  if channel != "nightly"
+                                  else "nightly.cache")
 
-        now = datetime.now()
-        if (not os.path.exists(prefs_path) or
-            (datetime.fromtimestamp(os.stat(prefs_path).st_mtime) <
-             now - timedelta(days=2))):
-            with open(prefs_path, "wb") as f:
-                resp = get("https://hg.mozilla.org/mozilla-central/raw-file/tip/testing/profiles/prefs_general.js")
+        have_cache = False
+        if os.path.exists(cache_file):
+            if channel != "nightly":
+                have_cache = True
+            else:
+                now = datetime.now()
+                have_cache = (datetime.fromtimestamp(os.stat(cache_file).st_mtime) >
+                              now - timedelta(days=1))
+
+        # If we don't have a recent download, grab the url
+        if not have_cache:
+            url = self.get_prefs_url(version, channel)
+
+            with open(cache_file, "wb") as f:
+                print("Installing test prefs from %s" % url)
+                resp = get(url)
                 f.write(resp.content)
+        else:
+            print("Using cached test prefs from %s" % cache_file)
+
+        shutil.copyfile(cache_file, prefs_file)
 
         return dest
 
diff --git a/tools/wpt/run.py b/tools/wpt/run.py
index 6b39eb9..55a6ab6 100644
--- a/tools/wpt/run.py
+++ b/tools/wpt/run.py
@@ -98,8 +98,7 @@
     if product not in ("firefox", "servo"):
         config = serve.load_config(os.path.join(wpt_root, "config.default.json"),
                                    os.path.join(wpt_root, "config.json"))
-        expected_hosts = (set(config["domains"].itervalues()) ^
-                          set(config["not_domains"].itervalues()))
+        expected_hosts = set(config.all_domains_set)
         missing_hosts = set(expected_hosts)
         if platform.uname()[0] != "Windows":
             hosts_path = "/etc/hosts"
@@ -201,8 +200,7 @@
                 kwargs["test_types"].remove("wdspec")
 
         if kwargs["prefs_root"] is None:
-            print("Downloading gecko prefs")
-            prefs_root = self.browser.install_prefs(self.venv.path)
+            prefs_root = self.browser.install_prefs(kwargs["binary"], self.venv.path)
             kwargs["prefs_root"] = prefs_root
 
 
@@ -458,7 +456,8 @@
 
 def run_single(venv, **kwargs):
     from wptrunner import wptrunner
-    return wptrunner.start(**kwargs)
+    wptrunner.start(**kwargs)
+    return
 
 
 def main():
diff --git a/tools/wptrunner/requirements_firefox.txt b/tools/wptrunner/requirements_firefox.txt
index 8b06373..0eb26e7 100644
--- a/tools/wptrunner/requirements_firefox.txt
+++ b/tools/wptrunner/requirements_firefox.txt
@@ -1,8 +1,8 @@
-marionette_driver == 2.5.0
-mozprofile == 0.29
+marionette_driver==2.6.0
+mozprofile==1.1.0
 mozprocess == 0.26
 mozcrash == 1.0
-mozrunner==6.15
+mozrunner == 7.0.0
 mozleak == 0.1
 mozinstall == 1.15
 mozdownload == 1.23
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_ref_timeout-ref.html b/tools/wptrunner/test/testdata/reftest/reftest_ref_timeout-ref.html
deleted file mode 100644
index 04cbb71..0000000
--- a/tools/wptrunner/test/testdata/reftest/reftest_ref_timeout-ref.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<html class="reftest-wait">
-<title>rel=match that should time out in the ref</title>
-<link rel=match href=reftest_ref_timeout-ref.html>
-<style>
-:root {background-color:green}
-</style>
diff --git a/tools/wptrunner/test/testdata/reftest/reftest_wait_0.html b/tools/wptrunner/test/testdata/reftest/reftest_wait_0.html
deleted file mode 100644
index 4f92715..0000000
--- a/tools/wptrunner/test/testdata/reftest/reftest_wait_0.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<html class="reftest-wait">
-<title>rel=match that should fail</title>
-<link rel=match href=red.html>
-<style>
-:root {background-color:red}
-</style>
-<script>
-setTimeout(function() {
-  document.documentElement.style.backgroundColor = "green";
-  document.documentElement.className = "";
-}, 2000);
-</script>
-</html>
diff --git a/tools/wptrunner/wptrunner/browsers/firefox.py b/tools/wptrunner/wptrunner/browsers/firefox.py
index 0a0ca7e..7348b15 100644
--- a/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -102,7 +102,7 @@
         if kwargs["binary_args"]:
             options["args"] = kwargs["binary_args"]
         options["prefs"] = {
-            "network.dns.localDomains": ",".join(server_config.domains.itervalues())
+            "network.dns.localDomains": ",".join(server_config.domains_set)
         }
         capabilities["moz:firefoxOptions"] = options
     if kwargs["certutil_binary"] is None:
@@ -198,7 +198,7 @@
         self.profile = FirefoxProfile(preferences=preferences)
         self.profile.set_preferences({"marionette.port": self.marionette_port,
                                       "dom.disable_open_during_load": False,
-                                      "network.dns.localDomains": ",".join(self.config.domains.itervalues()),
+                                      "network.dns.localDomains": ",".join(self.config.domains_set),
                                       "network.proxy.type": 0,
                                       "places.history.enabled": False,
                                       "dom.send_after_paint_to_content": True,
diff --git a/tools/wptrunner/wptrunner/browsers/sauce.py b/tools/wptrunner/wptrunner/browsers/sauce.py
index 9ae2f7e..709af41 100644
--- a/tools/wptrunner/wptrunner/browsers/sauce.py
+++ b/tools/wptrunner/wptrunner/browsers/sauce.py
@@ -166,7 +166,7 @@
             "--metrics-address=0.0.0.0:9876",
             "--readyfile=./sauce_is_ready",
             "--tunnel-domains",
-            ",".join(self.env_config['domains'].values())
+            ",".join(self.env_config.domains_set)
         ])
 
         # Timeout config vars
diff --git a/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat b/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
index 4554894..9d0878e 100644
--- a/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
+++ b/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
@@ -1,2 +1,9 @@
 @echo off
 reg add "HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Storage\microsoft.microsoftedge_8wekyb3d8bbwe\MicrosoftEdge\New Windows" /v "PopupMgr" /t REG_SZ /d no
+
+
+REM Download and install the Ahem font
+REM - https://wiki.saucelabs.com/display/DOCS/Downloading+Files+to+a+Sauce+Labs+Virtual+Machine+Prior+to+Testing
+REM - https://superuser.com/questions/201896/how-do-i-install-a-font-from-the-windows-command-prompt
+bitsadmin.exe /transfer "JobName" https://github.com/w3c/web-platform-tests/raw/master/fonts/Ahem.ttf "%WINDIR%\Fonts\Ahem.ttf"
+reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Ahem (TrueType)" /t REG_SZ /d Ahem.ttf /f
diff --git a/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh b/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
index 85c72e6..06c48bd 100644
--- a/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
+++ b/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
@@ -1,2 +1,3 @@
 #!/bin/bash
+curl https://raw.githubusercontent.com/w3c/web-platform-tests/master/fonts/Ahem.ttf > ~/Library/Fonts/Ahem.ttf
 defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaScriptCanOpenWindowsAutomatically -bool true
diff --git a/tools/wptrunner/wptrunner/environment.py b/tools/wptrunner/wptrunner/environment.py
index 856a7e1..9ce3cda 100644
--- a/tools/wptrunner/wptrunner/environment.py
+++ b/tools/wptrunner/wptrunner/environment.py
@@ -135,6 +135,7 @@
 
     def load_config(self):
         default_config_path = os.path.join(serve_path(self.test_paths), "config.default.json")
+        override_path = os.path.join(serve_path(self.test_paths), "config.json")
 
         with open(default_config_path) as f:
             default_config = json.load(f)
@@ -144,8 +145,15 @@
         config.ports = {
             "http": [8000, 8001],
             "https": [8443],
-            "ws": [8888]
+            "ws": [8888],
+            "wss": [8889],
         }
+
+        if os.path.exists(override_path):
+            with open(override_path) as f:
+                override_obj = json.load(f)
+            config.update(override_obj)
+
         config.check_subdomains = False
         config.ssl = {}
 
@@ -214,7 +222,7 @@
 
     def ensure_started(self):
         # Pause for a while to ensure that the server has a chance to start
-        for _ in xrange(20):
+        for _ in xrange(60):
             failed = self.test_servers()
             if not failed:
                 return
diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py
index 80549af..3f6d1a2 100644
--- a/tools/wptrunner/wptrunner/executors/base.py
+++ b/tools/wptrunner/wptrunner/executors/base.py
@@ -153,10 +153,10 @@
         :param test: The test to run"""
         if test.environment != self.last_environment:
             self.on_environment_change(test.environment)
-
         try:
             result = self.do_test(test)
         except Exception as e:
+            self.logger.warning(traceback.format_exc(e))
             result = self.result_from_exception(test, e)
 
         if result is Stop:
@@ -193,7 +193,7 @@
         if hasattr(e, "status") and e.status in test.result_cls.statuses:
             status = e.status
         else:
-            status = "ERROR"
+            status = "INTERNAL-ERROR"
         message = unicode(getattr(e, "message", ""))
         if message:
             message += "\n"
@@ -406,7 +406,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", message)
+            self.result = False, ("INTERNAL-ERROR", message)
         finally:
             self.result_flag.set()
 
@@ -438,6 +438,8 @@
 class WebDriverProtocol(Protocol):
     server_cls = None
 
+    implements = [ConnectionlessBaseProtocolPart]
+
     def __init__(self, executor, browser):
         Protocol.__init__(self, executor, browser)
         self.webdriver_binary = executor.webdriver_binary
@@ -545,6 +547,7 @@
     def _send_message(self, message_type, status, message=None):
         self.protocol.testdriver.send_message(message_type, status, message=message)
 
+
 class ClickAction(object):
     def __init__(self, logger, protocol):
         self.logger = logger
@@ -560,6 +563,7 @@
         self.logger.debug("Clicking element: %s" % selector)
         self.protocol.click.element(elements[0])
 
+
 class SendKeysAction(object):
     def __init__(self, logger, protocol):
         self.logger = logger
diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py
index 60332a6..2c4bc59 100644
--- a/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -403,7 +403,7 @@
                 self.prefs.set(name, value)
 
         for name, value in new_environment.get("prefs", {}).iteritems():
-            self.executor.original_pref_values[name] = self.get_pref(name)
+            self.executor.original_pref_values[name] = self.prefs.get(name)
             self.prefs.set(name, value)
 
 
@@ -454,7 +454,7 @@
             # We didn't get any data back from the test, so check if the
             # browser is still responsive
             if self.protocol.is_alive:
-                self.result = False, ("ERROR", None)
+                self.result = False, ("INTERNAL-ERROR", None)
             else:
                 self.result = False, ("CRASH", None)
         return self.result
@@ -475,7 +475,8 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.logger.warning(message)
+            self.result = False, ("INTERNAL-ERROR", None)
 
         finally:
             self.result_flag.set()
@@ -551,6 +552,9 @@
         while True:
             result = protocol.base.execute_script(
                 self.script_resume % format_map, async=True)
+            if result is None:
+                # This can happen if we get an content process crash
+                return None
             done, rv = handler(result)
             if done:
                 break
@@ -639,12 +643,12 @@
                                      test_url,
                                      timeout).run()
 
-    def _screenshot(self, marionette, url, timeout):
-        marionette.navigate(url)
+    def _screenshot(self, protocol, url, timeout):
+        protocol.marionette.navigate(url)
 
-        marionette.execute_async_script(self.wait_script)
+        protocol.base.execute_script(self.wait_script, async=True)
 
-        screenshot = marionette.screenshot(full=False)
+        screenshot = protocol.marionette.screenshot(full=False)
         # strip off the data:img/png, part of the url
         if screenshot.startswith("data:image/png;base64,"):
             screenshot = screenshot.split(",", 1)[1]
diff --git a/tools/wptrunner/wptrunner/executors/executorselenium.py b/tools/wptrunner/wptrunner/executors/executorselenium.py
index 94ef10b..98c49bd 100644
--- a/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -245,7 +245,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.result = False, ("INTERNAL-ERROR", e)
         finally:
             self.result_flag.set()
 
diff --git a/tools/wptrunner/wptrunner/executors/executorservodriver.py b/tools/wptrunner/wptrunner/executors/executorservodriver.py
index 8f2964a..626c987 100644
--- a/tools/wptrunner/wptrunner/executors/executorservodriver.py
+++ b/tools/wptrunner/wptrunner/executors/executorservodriver.py
@@ -109,7 +109,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.result = False, ("INTERNAL-ERROR", e)
         finally:
             self.result_flag.set()
 
@@ -214,7 +214,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            return test.result_cls("ERROR", message), []
+            return test.result_cls("INTERNAL-ERROR", message), []
 
     def screenshot(self, test, viewport_size, dpi):
         # https://github.com/w3c/wptrunner/issues/166
diff --git a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
index 6379929..c1936eb 100644
--- a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
+++ b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
@@ -62,7 +62,7 @@
                          path],
                         plugins=[harness, subtests])
         except Exception as e:
-            harness.outcome = ("ERROR", str(e))
+            harness.outcome = ("INTERNAL-ERROR", str(e))
 
     return (harness.outcome, subtests.results)
 
diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py
index 1d2d768..257e789 100644
--- a/tools/wptrunner/wptrunner/testrunner.py
+++ b/tools/wptrunner/wptrunner/testrunner.py
@@ -560,13 +560,18 @@
                                     expected=expected,
                                     stack=result.stack)
 
-        # TODO: consider changing result if there is a crash dump file
-
-        # Write the result of the test harness
+        # We have a couple of status codes that are used internally, but not exposed to the
+        # user. These are used to indicate that some possibly-broken state was reached
+        # and we should restart the runner before the next test.
+        # INTERNAL-ERROR indicates a Python exception was caught in the harness
+        # EXTERNAL-TIMEOUT indicates we had to forcibly kill the browser from the harness
+        # because the test didn't return a result after reaching the test-internal timeout
+        status_subns = {"INTERNAL-ERROR": "ERROR",
+                        "EXTERNAL-TIMEOUT": "TIMEOUT"}
         expected = test.expected()
-        status = file_result.status if file_result.status != "EXTERNAL-TIMEOUT" else "TIMEOUT"
+        status = status_subns.get(file_result.status, file_result.status)
 
-        if file_result.status in ("TIMEOUT", "EXTERNAL-TIMEOUT"):
+        if file_result.status in ("TIMEOUT", "EXTERNAL-TIMEOUT", "INTERNAL-ERROR"):
             if self.browser.check_for_crashes():
                 status = "CRASH"
 
@@ -585,11 +590,12 @@
                              extra=file_result.extra)
 
         restart_before_next = (test.restart_after or
-                               file_result.status in ("CRASH", "EXTERNAL-TIMEOUT") or
+                               file_result.status in ("CRASH", "EXTERNAL-TIMEOUT", "INTERNAL-ERROR") or
                                ((subtest_unexpected or is_unexpected) and
                                 self.restart_on_unexpected))
 
-        if (self.pause_after_test or
+        if (not file_result.status == "CRASH" and
+            self.pause_after_test or
             (self.pause_on_unexpected and (subtest_unexpected or is_unexpected))):
             self.logger.info("Pausing until the browser exits")
             self.send_message("wait")
diff --git a/tools/wptrunner/wptrunner/tests/browsers/test_sauce.py b/tools/wptrunner/wptrunner/tests/browsers/test_sauce.py
index aa1bc01..db0de8f 100644
--- a/tools/wptrunner/wptrunner/tests/browsers/test_sauce.py
+++ b/tools/wptrunner/wptrunner/tests/browsers/test_sauce.py
@@ -110,6 +110,7 @@
             sauce_connect_binary="ddd")
 
         env_config = Config(browser_host="example.net",
+                            alternate_hosts={"alt": "example.org"},
                             subdomains={"a", "b"},
                             not_subdomains={"x", "y"})
         sauce_connect(None, env_config)
@@ -125,4 +126,7 @@
                 assert rest[1].startswith("-"), "--tunnel-domains takes a comma separated list (not a space separated list)"
             assert set(rest[0].split(",")) == {'example.net',
                                                'a.example.net',
-                                               'b.example.net'}
+                                               'b.example.net',
+                                               'example.org',
+                                               'a.example.org',
+                                               'b.example.org'}
diff --git a/tools/wptrunner/wptrunner/wpttest.py b/tools/wptrunner/wptrunner/wpttest.py
index 9f4c1be..0fb1bdd 100644
--- a/tools/wptrunner/wptrunner/wpttest.py
+++ b/tools/wptrunner/wptrunner/wpttest.py
@@ -36,7 +36,7 @@
 
 class TestharnessResult(Result):
     default_expected = "OK"
-    statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["OK", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
 
 
 class TestharnessSubtestResult(SubtestResult):
@@ -46,12 +46,13 @@
 
 class ReftestResult(Result):
     default_expected = "PASS"
-    statuses = set(["PASS", "FAIL", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["PASS", "FAIL", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT",
+                    "CRASH"])
 
 
 class WdspecResult(Result):
     default_expected = "OK"
-    statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["OK", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
 
 
 class WdspecSubtestResult(SubtestResult):
diff --git a/tools/wptserve/tests/functional/docroot/sub_file_hash.sub.txt b/tools/wptserve/tests/functional/docroot/sub_file_hash.sub.txt
new file mode 100644
index 0000000..369ac8a
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_file_hash.sub.txt
@@ -0,0 +1,6 @@
+md5: {{file_hash(md5, sub_file_hash_subject.txt)}}
+sha1: {{file_hash(sha1, sub_file_hash_subject.txt)}}
+sha224: {{file_hash(sha224, sub_file_hash_subject.txt)}}
+sha256: {{file_hash(sha256, sub_file_hash_subject.txt)}}
+sha384: {{file_hash(sha384, sub_file_hash_subject.txt)}}
+sha512: {{file_hash(sha512, sub_file_hash_subject.txt)}}
diff --git a/tools/wptserve/tests/functional/docroot/sub_file_hash_subject.txt b/tools/wptserve/tests/functional/docroot/sub_file_hash_subject.txt
new file mode 100644
index 0000000..d567d28
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_file_hash_subject.txt
@@ -0,0 +1,2 @@
+This file is used to verify expected behavior of the `file_hash` "sub"
+function.
diff --git a/tools/wptserve/tests/functional/docroot/sub_file_hash_unrecognized.sub.txt b/tools/wptserve/tests/functional/docroot/sub_file_hash_unrecognized.sub.txt
new file mode 100644
index 0000000..5f1281d
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_file_hash_unrecognized.sub.txt
@@ -0,0 +1 @@
+{{file_hash(sha007, sub_file_hash_subject.txt)}}
diff --git a/tools/wptserve/tests/functional/docroot/sub_location.sub.txt b/tools/wptserve/tests/functional/docroot/sub_location.sub.txt
new file mode 100644
index 0000000..6129abd
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_location.sub.txt
@@ -0,0 +1,8 @@
+host: {{location[host]}}
+hostname: {{location[hostname]}}
+path: {{location[path]}}
+pathname: {{location[pathname]}}
+port: {{location[port]}}
+query: {{location[query]}}
+scheme: {{location[scheme]}}
+server: {{location[server]}}
diff --git a/tools/wptserve/tests/functional/docroot/sub_url_base.sub.txt b/tools/wptserve/tests/functional/docroot/sub_url_base.sub.txt
new file mode 100644
index 0000000..889cd07
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_url_base.sub.txt
@@ -0,0 +1 @@
+Before {{url_base}} After
diff --git a/tools/wptserve/tests/functional/docroot/sub_uuid.sub.txt b/tools/wptserve/tests/functional/docroot/sub_uuid.sub.txt
new file mode 100644
index 0000000..fd968fe
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_uuid.sub.txt
@@ -0,0 +1 @@
+Before {{uuid()}} After
diff --git a/tools/wptserve/tests/functional/docroot/sub_var.sub.txt b/tools/wptserve/tests/functional/docroot/sub_var.sub.txt
new file mode 100644
index 0000000..9492ec1
--- /dev/null
+++ b/tools/wptserve/tests/functional/docroot/sub_var.sub.txt
@@ -0,0 +1 @@
+{{$first:host}} {{$second:ports[http][0]}} A {{$second}} B {{$first}} C
diff --git a/tools/wptserve/tests/functional/test_pipes.py b/tools/wptserve/tests/functional/test_pipes.py
index 147ad15..069bd76 100644
--- a/tools/wptserve/tests/functional/test_pipes.py
+++ b/tools/wptserve/tests/functional/test_pipes.py
@@ -1,5 +1,6 @@
 import os
 import unittest
+import urllib2
 import time
 import json
 
@@ -58,16 +59,59 @@
         expected = "localhost localhost %i" % self.server.port
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_file_hash(self):
+        resp = self.request("/sub_file_hash.sub.txt")
+        expected = """
+md5: JmI1W8fMHfSfCarYOSxJcw==
+sha1: nqpWqEw4IW8NjD6R375gtrQvtTo=
+sha224: RqQ6fMmta6n9TuA/vgTZK2EqmidqnrwBAmQLRQ==
+sha256: G6Ljg1uPejQxqFmvFOcV/loqnjPTW5GSOePOfM/u0jw=
+sha384: lkXHChh1BXHN5nT5BYhi1x67E1CyYbPKRKoF2LTm5GivuEFpVVYtvEBHtPr74N9E
+sha512: r8eLGRTc7ZznZkFjeVLyo6/FyQdra9qmlYCwKKxm3kfQAswRS9+3HsYk3thLUhcFmmWhK4dXaICz
+JwGFonfXwg=="""
+        self.assertEqual(resp.read().rstrip(), expected.strip())
+
+    def test_sub_file_hash_unrecognized(self):
+        with self.assertRaises(urllib2.HTTPError):
+            self.request("/sub_file_hash_unrecognized.sub.txt")
+
     def test_sub_headers(self):
         resp = self.request("/sub_headers.txt", query="pipe=sub", headers={"X-Test": "PASS"})
         expected = "PASS"
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_location(self):
+        resp = self.request("/sub_location.sub.txt?query_string")
+        expected = """
+host: localhost:{0}
+hostname: localhost
+path: /sub_location.sub.txt
+pathname: /sub_location.sub.txt
+port: {0}
+query: ?query_string
+scheme: http
+server: http://localhost:{0}""".format(self.server.port)
+        self.assertEqual(resp.read().rstrip(), expected.strip())
+
     def test_sub_params(self):
         resp = self.request("/sub_params.txt", query="test=PASS&pipe=sub")
         expected = "PASS"
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_url_base(self):
+        resp = self.request("/sub_url_base.sub.txt")
+        self.assertEqual(resp.read().rstrip(), "Before / After")
+
+    def test_sub_uuid(self):
+        resp = self.request("/sub_uuid.sub.txt")
+        self.assertRegexpMatches(resp.read().rstrip(), r"Before [a-f0-9-]+ After")
+
+    def test_sub_var(self):
+        resp = self.request("/sub_var.sub.txt")
+        port = self.server.port
+        expected = "localhost %s A %s B localhost C" % (port, port)
+        self.assertEqual(resp.read().rstrip(), expected)
+
 class TestTrickle(TestUsingServer):
     def test_trickle(self):
         #Actually testing that the response trickles in is not that easy
diff --git a/tools/wptserve/tests/test_config.py b/tools/wptserve/tests/test_config.py
index c9c270b..2d4bed5 100644
--- a/tools/wptserve/tests/test_config.py
+++ b/tools/wptserve/tests/test_config.py
@@ -272,48 +272,128 @@
 
 def test_domains():
     c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
                       subdomains={"a", "b"},
                       not_subdomains={"x", "y"})
     domains = c.domains
     assert domains == {
-        "": "foo.bar",
-        "a": "a.foo.bar",
-        "b": "b.foo.bar",
+        "": {
+            "": "foo.bar",
+            "a": "a.foo.bar",
+            "b": "b.foo.bar",
+        },
+        "alt": {
+            "": "foo2.bar",
+            "a": "a.foo2.bar",
+            "b": "b.foo2.bar",
+        },
     }
 
 
 def test_not_domains():
     c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
                       subdomains={"a", "b"},
                       not_subdomains={"x", "y"})
     not_domains = c.not_domains
     assert not_domains == {
-        "x": "x.foo.bar",
-        "y": "y.foo.bar",
+        "": {
+            "x": "x.foo.bar",
+            "y": "y.foo.bar",
+        },
+        "alt": {
+            "x": "x.foo2.bar",
+            "y": "y.foo2.bar",
+        },
     }
 
 
 def test_domains_not_domains_intersection():
     c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
                       subdomains={"a", "b"},
                       not_subdomains={"x", "y"})
     domains = c.domains
     not_domains = c.not_domains
-    assert len(set(domains.iterkeys()) & set(not_domains.iterkeys())) == 0
-    assert len(set(domains.itervalues()) & set(not_domains.itervalues())) == 0
+    assert len(set(domains.iterkeys()) ^ set(not_domains.iterkeys())) == 0
+    for host in domains.iterkeys():
+        host_domains = domains[host]
+        host_not_domains = not_domains[host]
+        assert len(set(host_domains.iterkeys()) & set(host_not_domains.iterkeys())) == 0
+        assert len(set(host_domains.itervalues()) & set(host_not_domains.itervalues())) == 0
 
 
 def test_all_domains():
     c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
                       subdomains={"a", "b"},
                       not_subdomains={"x", "y"})
     all_domains = c.all_domains
     assert all_domains == {
-        "": "foo.bar",
-        "a": "a.foo.bar",
-        "b": "b.foo.bar",
-        "x": "x.foo.bar",
-        "y": "y.foo.bar",
+        "": {
+            "": "foo.bar",
+            "a": "a.foo.bar",
+            "b": "b.foo.bar",
+            "x": "x.foo.bar",
+            "y": "y.foo.bar",
+        },
+        "alt": {
+            "": "foo2.bar",
+            "a": "a.foo2.bar",
+            "b": "b.foo2.bar",
+            "x": "x.foo2.bar",
+            "y": "y.foo2.bar",
+        },
+    }
+
+
+def test_domains_set():
+    c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
+                      subdomains={"a", "b"},
+                      not_subdomains={"x", "y"})
+    domains_set = c.domains_set
+    assert domains_set == {
+        "foo.bar",
+        "a.foo.bar",
+        "b.foo.bar",
+        "foo2.bar",
+        "a.foo2.bar",
+        "b.foo2.bar",
+    }
+
+
+def test_not_domains_set():
+    c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
+                      subdomains={"a", "b"},
+                      not_subdomains={"x", "y"})
+    not_domains_set = c.not_domains_set
+    assert not_domains_set == {
+        "x.foo.bar",
+        "y.foo.bar",
+        "x.foo2.bar",
+        "y.foo2.bar",
+    }
+
+
+def test_all_domains_set():
+    c = config.Config(browser_host="foo.bar",
+                      alternate_hosts={"alt": "foo2.bar"},
+                      subdomains={"a", "b"},
+                      not_subdomains={"x", "y"})
+    all_domains_set = c.all_domains_set
+    assert all_domains_set == {
+        "foo.bar",
+        "a.foo.bar",
+        "b.foo.bar",
+        "x.foo.bar",
+        "y.foo.bar",
+        "foo2.bar",
+        "a.foo2.bar",
+        "b.foo2.bar",
+        "x.foo2.bar",
+        "y.foo2.bar",
     }
 
 
diff --git a/tools/wptserve/tests/test_replacement_tokenizer.py b/tools/wptserve/tests/test_replacement_tokenizer.py
index 4bc1e6d..ea0c0c2 100644
--- a/tools/wptserve/tests/test_replacement_tokenizer.py
+++ b/tools/wptserve/tests/test_replacement_tokenizer.py
@@ -6,7 +6,7 @@
     "content,expected",
     [
         ["aaa", [('ident', 'aaa')]],
-        ["bbb()", [('ident', 'bbb()')]],
+        ["bbb()", [('ident', 'bbb'), ('arguments', [])]],
         ["$ccc:ddd", [('var', '$ccc'), ('ident', 'ddd')]],
         ["$eee", [('ident', '$eee')]],
         ["fff[0]", [('ident', 'fff'), ('index', 0)]],
diff --git a/tools/wptserve/wptserve/config.py b/tools/wptserve/wptserve/config.py
index b760724..a36f5f9 100644
--- a/tools/wptserve/wptserve/config.py
+++ b/tools/wptserve/wptserve/config.py
@@ -14,6 +14,8 @@
 _renamed_props = {
     "host": "browser_host",
     "bind_hostname": "bind_address",
+    "external_host": "server_host",
+    "host_ip": "server_host",
 }
 
 
@@ -116,13 +118,15 @@
 
     @property
     def ports(self):
+        # To make this method thread-safe, we write to a temporary dict first,
+        # and change self._computed_ports to the new dict at last atomically.
+        new_ports = defaultdict(list)
+
         try:
             old_ports = self._computed_ports
         except AttributeError:
             old_ports = {}
 
-        self._computed_ports = defaultdict(list)
-
         for scheme, ports in self._ports.iteritems():
             for i, port in enumerate(ports):
                 if scheme in ["wss", "https"] and not self.ssl_env.ssl_enabled:
@@ -134,8 +138,9 @@
                         port = get_port(self.server_host)
                 else:
                     port = port
-                self._computed_ports[scheme].append(port)
+                new_ports[scheme].append(port)
 
+        self._computed_ports = new_ports
         return self._computed_ports
 
     @ports.setter
@@ -171,24 +176,52 @@
 
     @property
     def domains(self):
-        assert self.browser_host.encode("idna") == self.browser_host
-        domains = {subdomain: (subdomain.encode("idna") + u"." + self.browser_host)
-                   for subdomain in self.subdomains}
-        domains[""] = self.browser_host
-        return domains
+        hosts = self.alternate_hosts.copy()
+        assert "" not in hosts
+        hosts[""] = self.browser_host
+
+        rv = {}
+        for name, host in hosts.iteritems():
+            rv[name] = {subdomain: (subdomain.encode("idna") + u"." + host)
+                        for subdomain in self.subdomains}
+            rv[name][""] = host
+        return rv
 
     @property
     def not_domains(self):
-        assert self.browser_host.encode("idna") == self.browser_host
-        domains = {subdomain: (subdomain.encode("idna") + u"." + self.browser_host)
-                   for subdomain in self.not_subdomains}
-        return domains
+        hosts = self.alternate_hosts.copy()
+        assert "" not in hosts
+        hosts[""] = self.browser_host
+
+        rv = {}
+        for name, host in hosts.iteritems():
+            rv[name] = {subdomain: (subdomain.encode("idna") + u"." + host)
+                        for subdomain in self.not_subdomains}
+        return rv
 
     @property
     def all_domains(self):
-        domains = self.domains.copy()
-        domains.update(self.not_domains)
-        return domains
+        rv = self.domains.copy()
+        nd = self.not_domains
+        for host in rv:
+            rv[host].update(nd[host])
+        return rv
+
+    @property
+    def domains_set(self):
+        return {domain
+                for per_host_domains in self.domains.itervalues()
+                for domain in per_host_domains.itervalues()}
+
+    @property
+    def not_domains_set(self):
+        return {domain
+                for per_host_domains in self.not_domains.itervalues()
+                for domain in per_host_domains.itervalues()}
+
+    @property
+    def all_domains_set(self):
+        return self.domains_set | self.not_domains_set
 
     @property
     def ssl_env(self):
@@ -214,7 +247,7 @@
 
     @property
     def ssl_config(self):
-        key_path, cert_path = self.ssl_env.host_cert_path(self.domains)
+        key_path, cert_path = self.ssl_env.host_cert_path(self.domains.itervalues())
         return {"key_path": key_path,
                 "cert_path": cert_path,
                 "encrypt_after_connect": self.ssl["encrypt_after_connect"]}
diff --git a/tools/wptserve/wptserve/handlers.py b/tools/wptserve/wptserve/handlers.py
index 661df6d..4536c06 100644
--- a/tools/wptserve/wptserve/handlers.py
+++ b/tools/wptserve/wptserve/handlers.py
@@ -260,6 +260,8 @@
     def __call__(self, request, response):
         try:
             rv = self.func(request, response)
+        except HTTPException:
+            raise
         except Exception:
             msg = traceback.format_exc()
             raise HTTPException(500, message=msg)
diff --git a/tools/wptserve/wptserve/pipes.py b/tools/wptserve/wptserve/pipes.py
index 126a68e..9c13965 100644
--- a/tools/wptserve/wptserve/pipes.py
+++ b/tools/wptserve/wptserve/pipes.py
@@ -1,5 +1,8 @@
 from cgi import escape
+from collections import deque
 import gzip as gzip_module
+import hashlib
+import os
 import re
 import time
 import types
@@ -277,6 +280,10 @@
 
 
 class ReplacementTokenizer(object):
+    def arguments(self, token):
+        unwrapped = token[1:-1]
+        return ("arguments", re.split(r",\s*", token[1:-1]) if unwrapped else [])
+
     def ident(self, token):
         return ("ident", token)
 
@@ -296,8 +303,9 @@
         return self.scanner.scan(string)[0]
 
     scanner = re.Scanner([(r"\$\w+:", var),
-                          (r"\$?\w+(?:\(\))?", ident),
-                          (r"\[[^\]]*\]", index)])
+                          (r"\$?\w+", ident),
+                          (r"\[[^\]]*\]", index),
+                          (r"\([^)]*\)", arguments)])
 
 
 class FirstWrapper(object):
@@ -339,6 +347,11 @@
       A dictionary of query parameters supplied with the request.
     uuid()
       A pesudo-random UUID suitable for usage with stash
+    file_hash(algorithm, filepath)
+      The cryptographic hash of a file. Supported algorithms: md5, sha1,
+      sha224, sha256, sha384, and sha512. For example:
+
+        {{file_hash(md5, dom/interfaces.html)}}
 
     So for example in a setup running on localhost with a www
     subdomain and a http server on ports 80 and 81::
@@ -351,7 +364,7 @@
     It is also possible to assign a value to a variable name, which must start with
     the $ character, using the ":" syntax e.g.
 
-    {{$id:uuid()}
+    {{$id:uuid()}}
 
     Later substitutions in the same file may then refer to the variable
     by name e.g.
@@ -365,6 +378,39 @@
     response.content = new_content
     return response
 
+class SubFunctions(object):
+    @staticmethod
+    def uuid(request):
+        return str(uuid.uuid4())
+
+    # Maintain a whitelist of supported algorithms, restricted to those that
+    # are available on all platforms [1]. This ensures that test authors do not
+    # unknowingly introduce platform-specific tests.
+    #
+    # [1] https://docs.python.org/2/library/hashlib.html
+    supported_algorithms = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")
+
+    @staticmethod
+    def file_hash(request, algorithm, path):
+        if algorithm not in SubFunctions.supported_algorithms:
+            raise ValueError("Unsupported encryption algorithm: '%s'" % algorithm)
+
+        hash_obj = getattr(hashlib, algorithm)()
+        absolute_path = os.path.join(request.doc_root, path)
+
+        try:
+            with open(absolute_path) as f:
+                hash_obj.update(f.read())
+        except IOError:
+            # In this context, an unhandled IOError will be interpreted by the
+            # server as an indication that the template file is non-existent.
+            # Although the generic "Exception" is less precise, it avoids
+            # triggering a potentially-confusing HTTP 404 error in cases where
+            # the path to the file to be hashed is invalid.
+            raise Exception('Cannot open file for hash computation: "%s"' % absolute_path)
+
+        return hash_obj.digest().encode('base64').strip()
+
 def template(request, content, escape_type="html"):
     #TODO: There basically isn't any error handling here
     tokenizer = ReplacementTokenizer()
@@ -375,33 +421,35 @@
         content, = match.groups()
 
         tokens = tokenizer.tokenize(content)
+        tokens = deque(tokens)
 
-        if tokens[0][0] == "var":
-            variable = tokens[0][1]
-            tokens = tokens[1:]
+        token_type, field = tokens.popleft()
+
+        if token_type == "var":
+            variable = field
+            token_type, field = tokens.popleft()
         else:
             variable = None
 
-        assert tokens[0][0] == "ident" and all(item[0] == "index" for item in tokens[1:]), tokens
-
-        field = tokens[0][1]
+        if token_type != "ident":
+            raise Exception("unexpected token type %s (token '%r'), expected ident" % (token_type, field))
 
         if field in variables:
             value = variables[field]
+        elif hasattr(SubFunctions, field):
+            value = getattr(SubFunctions, field)
         elif field == "headers":
             value = request.headers
         elif field == "GET":
             value = FirstWrapper(request.GET)
+        elif field == "hosts":
+            value = request.server.config.all_domains
         elif field == "domains":
-            if ('not_domains' in request.server.config and
-                    tokens[1][1] in request.server.config['not_domains']):
-                value = request.server.config['not_domains']
-            else:
-                value = request.server.config['domains']
+            value = request.server.config.all_domains[""]
         elif field == "host":
             value = request.server.config["browser_host"]
         elif field in request.server.config:
-            value = request.server.config[tokens[0][1]]
+            value = request.server.config[field]
         elif field == "location":
             value = {"server": "%s://%s:%s" % (request.url_parts.scheme,
                                                request.url_parts.hostname,
@@ -414,15 +462,21 @@
                      "path": request.url_parts.path,
                      "pathname": request.url_parts.path,
                      "query": "?%s" % request.url_parts.query}
-        elif field == "uuid()":
-            value = str(uuid.uuid4())
         elif field == "url_base":
             value = request.url_base
         else:
             raise Exception("Undefined template variable %s" % field)
 
-        for item in tokens[1:]:
-            value = value[item[1]]
+        while tokens:
+            ttype, field = tokens.popleft()
+            if ttype == "index":
+                value = value[field]
+            elif ttype == "arguments":
+                value = value(request, *field)
+            else:
+                raise Exception(
+                    "unexpected token type %s (token '%r'), expected ident or arguments" % (ttype, field)
+                )
 
         assert isinstance(value, (int,) + types.StringTypes), tokens
 
diff --git a/tools/wptserve/wptserve/request.py b/tools/wptserve/wptserve/request.py
index 4476634..7f49681 100644
--- a/tools/wptserve/wptserve/request.py
+++ b/tools/wptserve/wptserve/request.py
@@ -348,12 +348,22 @@
 class RequestHeaders(dict):
     """Dictionary-like API for accessing request headers."""
     def __init__(self, items):
-        for key, value in zip(items.keys(), items.values()):
-            key = key.lower()
-            if key in self:
-                self[key].append(value)
+        for header in items.keys():
+            key = header.lower()
+            # get all headers with the same name
+            values = items.getallmatchingheaders(header)
+            if len(values) > 1:
+                # collect the multiple variations of the current header
+                multiples = []
+                # loop through the values from getallmatchingheaders
+                for value in values:
+                    # getallmatchingheaders returns raw header lines, so
+                    # split to get name, value
+                    multiples.append(value.split(':', 1)[1].strip())
+                dict.__setitem__(self, key, multiples)
             else:
-                dict.__setitem__(self, key, [value])
+                dict.__setitem__(self, key, [items[header]])
+
 
     def __getitem__(self, key):
         """Get all headers of a certain (case-insensitive) name. If there is
diff --git a/tools/wptserve/wptserve/server.py b/tools/wptserve/wptserve/server.py
index a678452..95d2320 100644
--- a/tools/wptserve/wptserve/server.py
+++ b/tools/wptserve/wptserve/server.py
@@ -13,6 +13,7 @@
 from six.moves.urllib.parse import urlsplit, urlunsplit
 
 from . import routes as default_routes
+from .config import Config
 from .logger import get_logger
 from .request import Server, Request
 from .response import Response
@@ -168,10 +169,8 @@
             Server.config = config
         else:
             self.logger.debug("Using default configuration")
-            Server.config = {"browser_host": server_address[0],
-                             "server_host": server_address[0],
-                             "domains": {"": server_address[0]},
-                             "ports": {"http": [self.server_address[1]]}}
+            Server.config = Config(browser_host=server_address[0],
+                                   ports={"http": [self.server_address[1]]})
 
 
         self.key_file = key_file
diff --git a/upgrade-insecure-requests/link-upgrade.sub.https.html b/upgrade-insecure-requests/link-upgrade.sub.https.html
index 6b315c8..46788e8 100644
--- a/upgrade-insecure-requests/link-upgrade.sub.https.html
+++ b/upgrade-insecure-requests/link-upgrade.sub.https.html
@@ -2,6 +2,7 @@
 <html>
 
   <head>
+    <meta name="timeout" content="long">
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
   </head>
@@ -56,7 +57,7 @@
           });
         });
 
-        test.step_timeout(function(){test.force_timeout()}, 5000);
+        test.step_timeout(function(){test.force_timeout()}, 10000);
       }
     </script>
   </body>
diff --git a/url/urlsearchparams-foreach.html b/url/urlsearchparams-foreach.html
index 33d1055..29c0ee8 100644
--- a/url/urlsearchparams-foreach.html
+++ b/url/urlsearchparams-foreach.html
@@ -41,7 +41,7 @@
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (const param of searchParams) {
+    for (param of searchParams) {
         if (param[0] === 'param0') {
             searchParams.delete('param1');
         }
@@ -56,7 +56,7 @@
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (const param of searchParams) {
+    for (param of searchParams) {
         if (param[0] === 'param0') {
             searchParams.delete('param0');
             // 'param1=1' is now in the first slot, so the next iteration will see 'param2=2'.
@@ -72,7 +72,7 @@
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (const param of searchParams) {
+    for (param of searchParams) {
         seen.push(param[0]);
         searchParams.delete(param[0]);
     }
diff --git a/user-timing/clearMarks.html b/user-timing/clearMarks.html
new file mode 100644
index 0000000..22d6726
--- /dev/null
+++ b/user-timing/clearMarks.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>functionality test of window.performance.clearMarks</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+<script>
+setup({ explicit_done: true });
+
+function onload_test()
+{
+    const entrylist_checker = new performance_entrylist_checker('mark');
+    const string_mark_names = mark_names.map(function (x) { return String(x)});
+    mark_names.forEach(performance.mark, performance);
+
+    for (let i = 0; i < mark_names.length; ++i)
+    {
+        performance.clearMarks(mark_names[i]);
+        const retained_entries = performance.getEntriesByType('mark');
+        const non_retained_entries = performance.getEntriesByName(mark_names[i], 'mark');
+        entrylist_checker.entrylist_check(retained_entries, mark_names.length - i - 1, string_mark_names,
+            'First loop: checking entries after removing "' + mark_names[i] + '". ');
+        test_equals(non_retained_entries.length, 0,
+            'First loop: marks that we cleared for "' + mark_names[i] + '" should not exist anymore.');
+    }
+
+    mark_names.forEach(performance.mark, performance);
+    performance.clearMarks();
+    test_equals(performance.getEntriesByType('mark').length, 0, 'No marks should exist after we clear all.');
+
+    // Following cases test clear existed mark name that is tied for two times.
+    mark_names.forEach(performance.mark, performance);
+    mark_names.forEach(performance.mark, performance);
+
+    for (let i = 0; i < mark_names.length; ++i)
+    {
+        performance.clearMarks(mark_names[i]);
+        const retained_entries = performance.getEntriesByType('mark');
+        const non_retained_entries = performance.getEntriesByName(mark_names[i], 'mark');
+        entrylist_checker.entrylist_check(retained_entries, (mark_names.length - i - 1) * 2, string_mark_names,
+            'Second loop: checking entries after removing "' + mark_names[i] + '". ');
+        test_equals(non_retained_entries.length, 0,
+            'Second loop: marks that we cleared for "' + mark_names[i] + '" should not exist anymore.');
+    }
+
+    // Following cases test clear functionality when mark names are tied for two times.
+    mark_names.forEach(performance.mark, performance);
+    mark_names.forEach(performance.mark, performance);
+    var entry_number_before_useless_clear = performance.getEntriesByType('Mark').length;
+    performance.clearMarks('NonExist');
+    var entry_number_after_useless_clear = performance.getEntriesByType('Mark').length;
+    test_equals(entry_number_before_useless_clear, entry_number_after_useless_clear, 'Nothing should happen if we clear a non-exist mark.');
+
+    performance.clearMarks();
+    test_equals(performance.getEntriesByType('mark').length, 0, 'No marks should exist when we clear all.');
+
+    done();
+}
+</script>
+</head>
+<body onload=onload_test()>
+    <h1>Description</h1>
+    <p>This test validates functionality of the interface window.performance.clearMarks.</p>
+    <div id="log"></div>
+</body>
+</html>
diff --git a/user-timing/clearMeasures.html b/user-timing/clearMeasures.html
new file mode 100644
index 0000000..488bece
--- /dev/null
+++ b/user-timing/clearMeasures.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>functionality test of window.performance.clearMeasures</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+<script>
+setup({ explicit_done: true });
+
+function onload_test()
+{
+    const context = new PerformanceContext(window.performance);
+    const entrylist_checker = new performance_entrylist_checker('measure');
+    const measure_names = measures.map(function(x) {return x[0];});
+
+    mark_names.forEach(context.mark, context);
+    measures.forEach(context.initialMeasures, context);
+    for (let i = 0; i < measures.length; ++i)
+    {
+        context.clearMeasures(measures[i][0]);
+        const retained_entries = context.getEntriesByType('measure');
+        const non_retained_entries = context.getEntriesByName(measures[i][0], 'measure');
+        entrylist_checker.entrylist_check(retained_entries, measures.length - i - 1, measure_names,
+            'First loop: checking entries after removing "' + measures[i][0] + '". ');
+        test_equals(non_retained_entries.length, 0,
+            'First loop: measure "' + measures[i][0] + '" should not exist anymore after we cleared it.');
+    }
+
+    measures.forEach(context.initialMeasures, context);
+    context.clearMeasures();
+    test_equals(context.getEntriesByType('measure').length, 0, 'No measures should exist after we clear all (after first loop).');
+
+    // Following cases test clear existed measure name that is tied twice.
+    measures.forEach(context.initialMeasures, context);
+    mark_names.forEach(context.mark, context);
+    measures.forEach(context.initialMeasures, context);
+    for (let i = 0; i < measures.length; ++i)
+    {
+        context.clearMeasures(measures[i][0]);
+        const retained_entries = context.getEntriesByType('measure');
+        const non_retained_entries = context.getEntriesByName(measures[i][0], 'measure');
+        entrylist_checker.entrylist_check(retained_entries, (measures.length - i - 1) * 2, measure_names,
+            'Second loop: checking entries after removing "' + measures[i][0] + '". ');
+        test_equals(non_retained_entries.length, 0,
+            'Second loop: measure "' + measures[i][0] +'" should not exist anymore after we cleared it.');
+    }
+
+    // Following cases test clear functionality when measure names are tied twice.
+    measures.forEach(context.initialMeasures, context);
+    measures.forEach(context.initialMeasures, context);
+    const entry_number_before_useless_clear = context.getEntriesByType('measure').length;
+    context.clearMeasures('NonExist');
+    const entry_number_after_useless_clear = context.getEntriesByType('measure').length;
+    test_equals(entry_number_before_useless_clear, entry_number_after_useless_clear, 'Nothing should happen if we clear a non-exist measure');
+    context.clearMeasures();
+    test_equals(context.getEntriesByType('measure').length, 0, 'No measures should exist when we clear all (after second loop).');
+
+    done();
+}
+</script>
+</head>
+<body onload=onload_test()>
+    <h1>Description</h1>
+    <p>This test validates functionality of the interface window.performance.clearMeasures.</p>
+    <div id="log"></div>
+</body>
+</html>
diff --git a/user-timing/mark.html b/user-timing/mark.html
new file mode 100644
index 0000000..a7074f1
--- /dev/null
+++ b/user-timing/mark.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>functionality test of window.performance.mark</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+<script>
+setup({ explicit_done: true });
+
+function onload_test()
+{
+    const entrylist_checker = new performance_entrylist_checker('mark');
+    const string_mark_names = mark_names.map(function (x) { return String(x)});
+
+    test_equals(performance.getEntriesByType("mark").length, 0, 'There should be ' + 0 + ' marks');
+    mark_names.forEach(performance.mark, performance);
+    let mark_entrylist = performance.getEntriesByType('mark');
+
+    entrylist_checker.entrylist_check(mark_entrylist, mark_names.length, string_mark_names, 'Checking all entries.');
+
+    for (let i = 0; i < mark_entrylist.length; ++i)
+    {
+        const mark_entrylist_by_name = performance.getEntriesByName(mark_entrylist[i].name, 'mark');
+        entrylist_checker.entrylist_check(mark_entrylist_by_name, 1, string_mark_names,
+            'First loop: checking entry of name "' + mark_entrylist[i].name + '".');
+    }
+
+    mark_names.forEach(performance.mark, performance);
+    mark_entrylist = performance.getEntriesByType('mark');
+    entrylist_checker.entrylist_check(mark_entrylist, mark_names.length * 2, string_mark_names, 'Checking all doubly marked entries.');
+
+    for (let i = 0; i < mark_entrylist.length; ++i)
+    {
+        const mark_entrylist_by_name = performance.getEntriesByName(mark_entrylist[i].name, 'mark');
+        entrylist_checker.entrylist_check(mark_entrylist_by_name, 2, string_mark_names,
+            'Second loop step ' + i + ': checking entries of name "' + mark_entrylist[i].name + '".');
+    }
+
+    done();
+}
+</script>
+</head>
+<body onload=onload_test()>
+    <h1>Description</h1>
+    <p>This test validates functionality of the interface window.performance.mark.</p>
+    <div id="log"></div>
+</body>
+</html>
diff --git a/user-timing/measure_associated_with_navigation_timing.html b/user-timing/measure_associated_with_navigation_timing.html
new file mode 100644
index 0000000..9fd4664
--- /dev/null
+++ b/user-timing/measure_associated_with_navigation_timing.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>functionality test of window.performance.measure</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+<script>
+setup({ explicit_done: true });
+
+function onload_test()
+{
+    const measures_for_timing_order = [
+        ['nav2now', 'navigationStart'],
+        ['loadTime', 'navigationStart', 'loadEventEnd'],
+        ['loadEventEnd2a', 'loadEventEnd', 'abc'],
+        ['nav2a', 'navigationStart', 'abc'],
+        ['domComplete2a', 'domComplete', 'abc'],
+        ['negativeValue', 1, 'navigationStart'],
+    ];
+    const context = new PerformanceContext(window.performance);
+
+    mark_names.forEach(context.mark, context);
+    measures_for_timing_order.forEach(context.initialMeasures, context);
+    test_greater_than(context.getEntriesByName('nav2now', 'measure')[0].duration, 0, 'Measure of navigationStart to now should be positive value.');
+    test_greater_than(context.getEntriesByName('loadTime', 'measure')[0].duration, 0, 'Measure of navigationStart to loadEventEnd should be positive value.');
+    test_greater_than(0, context.getEntriesByName('negativeValue', 'measure')[0].duration, 'Measure of current mark to navigationStart should be negative value.');
+    test_equals(context.getEntriesByName('loadTime', 'measure')[0].duration + context.getEntriesByName('loadEventEnd2a', 'measure')[0].duration, context.getEntriesByName('nav2a', 'measure')[0].duration, 'loadTime plus loadEventEnd to a mark "a" should equal to navigationStart to "a".');
+
+    // Following cases test for scenarios that measure names are tied twice.
+    mark_names.forEach(context.mark, context);
+    measures_for_timing_order.forEach(context.initialMeasures, context);
+
+    test_greater_than(context.getEntriesByName('nav2now', 'measure')[1].duration, context.getEntriesByName('nav2now', 'measure')[0].duration, 'Second measure of current mark to navigationStart should be negative value.');
+    test_equals(context.getEntriesByName('loadTime', 'measure')[0].duration, context.getEntriesByName('loadTime', 'measure')[1].duration, 'Measures of loadTime should have same duration.');
+    test_greater_than(context.getEntriesByName('domComplete2a', 'measure')[1].duration, context.getEntriesByName('domComplete2a', 'measure')[0].duration, 'Measure from domComplete event to most recent mark "a" should have longer duration.');
+    test_greater_than(context.getEntriesByName('negativeValue', 'measure')[0].duration, context.getEntriesByName('negativeValue', 'measure')[1].duration, 'Measure from most recent mark to navigationStart should have longer duration.');
+
+    done();
+}
+</script>
+</head>
+<body onload="setTimeout(onload_test,0)">
+    <h1>Description</h1>
+    <p>This test validates functionality of the interface window.performance.measure using keywords from the Navigation Timing spec.</p>
+    <div id="log"></div>
+</body>
+</html>
diff --git a/user-timing/measure_exception.html b/user-timing/measure_exception.html
new file mode 100644
index 0000000..8783ff7
--- /dev/null
+++ b/user-timing/measure_exception.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>exception test of window.performance.measure</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+</head>
+
+<body>
+<h1>Description</h1>
+<p>This test validates all exception scenarios of method window.performance.measure in User Timing API</p>
+
+<div id="log"></div>
+<script>
+performance.mark('ExistMark');
+test_method_throw_exception('performance.measure()', TypeError());
+test_method_throw_exception('performance.measure("Exception1", "NonExistMark1")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception2", "NonExistMark1", "navigationStart")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception3", "navigationStart", "NonExistMark1")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception4", "NonExistMark1", "ExistMark")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception5", "ExistMark", "NonExistMark1")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception6", "NonExistMark1", "NonExistMark2")', 'SYNTAX_ERR');
+test_method_throw_exception('performance.measure("Exception7", "redirectStart")', 'INVALID_ACCESS_ERR');
+</script>
+</body>
+</html>
diff --git a/user-timing/measures.html b/user-timing/measures.html
new file mode 100644
index 0000000..d0ab11e
--- /dev/null
+++ b/user-timing/measures.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>functionality test of window.performance.measure</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/performance-timeline-utils.js"></script>
+<script src="resources/webperftestharness.js"></script>
+<script src="resources/webperftestharnessextension.js"></script>
+<script>
+setup({ explicit_done: true });
+
+function onload_test()
+{
+    const context = new PerformanceContext(window.performance);
+    const entrylist_checker = new performance_entrylist_checker('measure');
+    const measure_names = measures.map(function(x) {return x[0];});
+
+    test_equals(context.getEntriesByType('measure').length, 0, 'There should be ' + 0 + ' entries returned.');
+
+    mark_names.forEach(context.mark, context);
+    measures.forEach(context.initialMeasures, context);
+
+    let measure_entrylist = context.getEntriesByType('measure');
+    entrylist_checker.entrylist_check(measure_entrylist, measures.length, measure_names,
+        'Checking all entries.');
+
+    for (let i = 0; i < measure_entrylist.length; ++i)
+    {
+        const measure_entrylist_by_name = context.getEntriesByName(measure_entrylist[i].name, 'measure');
+        entrylist_checker.entrylist_check(measure_entrylist_by_name, 1, measure_names,
+            'First loop: checking entry of name "' + measure_entrylist[i].name + '".');
+    }
+
+    // Following cases test for scenarios that measure names are tied for two times
+    mark_names.forEach(context.mark, context);
+    measures.forEach(context.initialMeasures, context);
+
+    measure_entrylist = context.getEntriesByType('measure');
+    entrylist_checker.entrylist_check(measure_entrylist, measures.length * 2, measure_names,
+        'Checking all doubly measured entries.');
+
+    for (let i = 0; i < measure_entrylist.length; ++i)
+    {
+        const measure_entrylist_by_name = context.getEntriesByName(measure_entrylist[i].name, 'measure');
+        entrylist_checker.entrylist_check(measure_entrylist_by_name, 2, measure_names,
+            'Second loop step ' + i + ': checking entry of name "' + measure_entrylist[i].name + '".');
+    }
+
+    done();
+}
+</script>
+</head>
+<body onload=onload_test()>
+    <h1>Description</h1>
+    <p>This test validates functionality of the interface window.performance.measure.</p>
+    <div id="log"></div>
+</body>
+</html>
diff --git a/user-timing/resources/webperftestharnessextension.js b/user-timing/resources/webperftestharnessextension.js
index 579ee90..822376b 100644
--- a/user-timing/resources/webperftestharnessextension.js
+++ b/user-timing/resources/webperftestharnessextension.js
@@ -88,28 +88,29 @@
         }
     }
 }
+
 function performance_entrylist_checker(type)
 {
-    var entryType = type;
+    const entryType = type;
 
-    function entry_check(entry, expectedNames)
+    function entry_check(entry, expectedNames, testDescription = '')
     {
-        var msg = 'Entry \"' + entry.name + '\" should be one that we have set.';
+        const msg = testDescription + 'Entry \"' + entry.name + '\" should be one that we have set.';
         wp_test(function() { assert_in_array(entry.name, expectedNames, msg); }, msg);
-        test_equals(entry.entryType, entryType, 'entryType should be \"' + entryType + '\".');
+        test_equals(entry.entryType, entryType, testDescription + 'entryType should be \"' + entryType + '\".');
         if (type === "measure") {
-            test_true(isFinite(entry.startTime), 'startTime should be a number.');
-            test_true(isFinite(entry.duration), 'duration should be a number.');
+            test_true(isFinite(entry.startTime), testDescription + 'startTime should be a number.');
+            test_true(isFinite(entry.duration), testDescription + 'duration should be a number.');
         } else if (type === "mark") {
-            test_greater_than(entry.startTime, 0, 'startTime should greater than 0.');
-            test_equals(entry.duration, 0, 'duration of mark should be 0.');
+            test_greater_than(entry.startTime, 0, testDescription + 'startTime should greater than 0.');
+            test_equals(entry.duration, 0, testDescription + 'duration of mark should be 0.');
         }
     }
 
     function entrylist_order_check(entryList)
     {
-        var inOrder = true;
-        for (var i = 0; i < entryList.length - 1; ++i)
+        let inOrder = true;
+        for (let i = 0; i < entryList.length - 1; ++i)
         {
             if (entryList[i + 1].startTime < entryList[i].startTime) {
                 inOrder = false;
@@ -119,13 +120,13 @@
         return inOrder;
     }
 
-    function entrylist_check(entryList, expectedLength, expectedNames)
+    function entrylist_check(entryList, expectedLength, expectedNames, testDescription = '')
     {
-        test_equals(entryList.length, expectedLength, 'There should be ' + expectedLength + ' entries.');
-        test_true(entrylist_order_check(entryList), 'Entries in entrylist should be in order.');
-        for (var i = 0; i < entryList.length; ++i)
+        test_equals(entryList.length, expectedLength, testDescription + 'There should be ' + expectedLength + ' entries.');
+        test_true(entrylist_order_check(entryList), testDescription + 'Entries in entrylist should be in order.');
+        for (let i = 0; i < entryList.length; ++i)
         {
-            entry_check(entryList[i], expectedNames);
+            entry_check(entryList[i], expectedNames, testDescription + 'Entry_list ' + i + '. ');
         }
     }
 
diff --git a/web-animations/animation-model/animation-types/property-list.js b/web-animations/animation-model/animation-types/property-list.js
index 400e12d..ec0c33f 100644
--- a/web-animations/animation-model/animation-types/property-list.js
+++ b/web-animations/animation-model/animation-types/property-list.js
@@ -425,7 +425,7 @@
       { type: 'discrete', options: [ [ '"a"', '"b"' ] ] }
     ],
     setup: t => {
-      return createPseudo(t, 'before');
+      return getPseudoElement(t, 'before');
     }
   },
   'counter-increment': {
@@ -529,8 +529,8 @@
     ]
   },
   'font-stretch': {
-    // https://drafts.csswg.org/css-fonts-3/#propdef-font-stretch
-    types: [ 'fontStretch' ]
+    // https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch
+    types: [ 'percentage' ]
   },
   'font-style': {
     // https://drafts.csswg.org/css-fonts/#propdef-font-style
diff --git a/web-animations/animation-model/animation-types/property-types.js b/web-animations/animation-model/animation-types/property-types.js
index 2849b08..60c7d2f 100644
--- a/web-animations/animation-model/animation-types/property-types.js
+++ b/web-animations/animation-model/animation-types/property-types.js
@@ -2566,54 +2566,6 @@
   },
 }
 
-const fontStretchType = {
-  testInterpolation: (property, setup) => {
-    test(t => {
-      const idlName = propertyToIDL(property);
-      const target = createTestElement(t, setup);
-      const animation =
-        target.animate({ [idlName]: ['ultra-condensed', 'extra-condensed'] },
-                       { duration: 1000, fill: 'both' });
-      testAnimationSamples(animation, idlName,
-                           [{ time: 499,  expected: 'ultra-condensed' },
-                            { time: 500,  expected: 'extra-condensed' }]);
-    }, `${property} supports animating as a font-stretch (adjacent values)`);
-
-    test(t => {
-      const idlName = propertyToIDL(property);
-      const target = createTestElement(t, setup);
-      const animation =
-        target.animate({ [idlName]: ['ultra-condensed', 'condensed'] },
-                       { duration: 1000, fill: 'both' });
-      testAnimationSamples(animation, idlName,
-                           [{ time: 500,  expected: 'extra-condensed' }]);
-    }, `${property} supports animating as a font-stretch (between value)`);
-  },
-
-  testAdditionOrAccumulation: (property, setup, composite) => {
-    test(t => {
-      const idlName = propertyToIDL(property);
-      const target = createTestElement(t, setup);
-      target.style[idlName] = 'condensed';
-      const animation =
-        target.animate({ [idlName]: ['expanded', 'ultra-expanded'] },
-                       { duration: 1000, composite });
-      testAnimationSamples(animation, idlName,
-                           [{ time: 0, expected: 'normal' },
-                            { time: 250, expected: 'semi-expanded' }]);
-    },
-    `${property} uses font-stretch behavior for composite type ${composite}`);
-  },
-
-  testAddition: function(property, setup) {
-    this.testAdditionOrAccumulation(property, setup, 'add');
-  },
-
-  testAccumulation: function(property, setup) {
-    this.testAdditionOrAccumulation(property, setup, 'accumulate');
-  },
-}
-
 const fontVariationSettingsType = {
   testInterpolation: (property, setup) => {
     test(t => {
@@ -2701,6 +2653,5 @@
   rect: rectType,
   position: positionType,
   dasharray: dasharrayType,
-  fontStretch: fontStretchType,
   fontVariationSettings: fontVariationSettingsType,
 };
diff --git a/web-animations/interfaces/Animatable/animate.html b/web-animations/interfaces/Animatable/animate.html
index c7b7b25..cbec1b0 100644
--- a/web-animations/interfaces/Animatable/animate.html
+++ b/web-animations/interfaces/Animatable/animate.html
@@ -205,13 +205,13 @@
 // Tests on CSSPseudoElement
 
 test(t => {
-  const pseudoTarget = createPseudo(t, 'before');
+  const pseudoTarget = getPseudoElement(t, 'before');
   const anim = pseudoTarget.animate(null);
   assert_class_string(anim, 'Animation', 'The returned object is an Animation');
 }, 'CSSPseudoElement.animate() creates an Animation object');
 
 test(t => {
-  const pseudoTarget = createPseudo(t, 'before');
+  const pseudoTarget = getPseudoElement(t, 'before');
   const anim = pseudoTarget.animate(null);
   assert_equals(anim.effect.target, pseudoTarget,
                 'The returned Animation targets to the correct object');
diff --git a/web-animations/interfaces/AnimationEffect/getComputedTiming.html b/web-animations/interfaces/AnimationEffect/getComputedTiming.html
index e0e4fc2..89466e2 100644
--- a/web-animations/interfaces/AnimationEffect/getComputedTiming.html
+++ b/web-animations/interfaces/AnimationEffect/getComputedTiming.html
@@ -15,10 +15,13 @@
 
   const ct = effect.getComputedTiming();
   assert_equals(ct.delay, 0, 'computed delay');
+  assert_equals(ct.endDelay, 0, 'computed endDelay');
   assert_equals(ct.fill, 'none', 'computed fill');
+  assert_equals(ct.iterationStart, 0.0, 'computed iterationStart');
   assert_equals(ct.iterations, 1.0, 'computed iterations');
   assert_equals(ct.duration, 0, 'computed duration');
   assert_equals(ct.direction, 'normal', 'computed direction');
+  assert_equals(ct.easing, 'linear', 'computed easing');
 }, 'values of getComputedTiming() when a KeyframeEffect is ' +
    'constructed without any KeyframeEffectOptions object');
 
@@ -28,15 +31,21 @@
     expected: { } },
   { desc:     'a normal KeyframeEffectOptions object',
     input:    { delay: 1000,
+                endDelay: 2000,
                 fill: 'auto',
+                iterationStart: 0.5,
                 iterations: 5.5,
                 duration: 'auto',
-                direction: 'alternate' },
+                direction: 'alternate',
+                easing: 'steps(2)' },
     expected: { delay: 1000,
+                endDelay: 2000,
                 fill: 'none',
+                iterationStart: 0.5,
                 iterations: 5.5,
                 duration: 0,
-                direction: 'alternate' } },
+                direction: 'alternate',
+                easing: 'steps(2)' } },
   { desc:     'a double value',
     input:    3000,
     timing:   { duration: 3000 },
@@ -78,14 +87,20 @@
     const ct = effect.getComputedTiming();
     assert_equals(ct.delay, expected('delay', 0),
                   'computed delay');
+    assert_equals(ct.endDelay, expected('endDelay', 0),
+                  'computed endDelay');
     assert_equals(ct.fill, expected('fill', 'none'),
                   'computed fill');
+    assert_equals(ct.iterationStart, expected('iterationStart', 0),
+                  'computed iterations');
     assert_equals(ct.iterations, expected('iterations', 1),
                   'computed iterations');
     assert_equals(ct.duration, expected('duration', 0),
                   'computed duration');
     assert_equals(ct.direction, expected('direction', 'normal'),
                   'computed direction');
+    assert_equals(ct.easing, expected('easing', 'linear'),
+                  'computed easing');
 
   }, 'values of getComputedTiming() when a KeyframeEffect is'
      + ` constructed by ${stest.desc}`);
diff --git a/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html b/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
index 133fd57..1b28210 100644
--- a/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
+++ b/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
@@ -308,6 +308,20 @@
    + ' specified');
 
 test(() => {
+  const test_error = { name: 'test' };
+  const bad_keyframe = { get left() { throw test_error; } };
+  assert_throws(test_error, () => {
+    new KeyframeEffect(null, createIterable([
+      { done: false, value: { left: '100px' } },
+      { done: false, value: bad_keyframe },
+      { done: false, value: { left: '200px' } },
+      { done: true },
+    ]));
+  });
+}, 'If a keyframe throws for an animatable property, that exception should be'
+    + ' propagated');
+
+test(() => {
   assert_throws({ name: 'TypeError' }, () => {
     new KeyframeEffect(null, createIterable([
       { done: false, value: { left: '100px' } },
@@ -321,6 +335,36 @@
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
+    { done: false, value: { left: '100px' } },
+    { done: false },  // No value member; keyframe is undefined.
+    { done: false, value: { left: '200px' } },
+    { done: true },
+  ]));
+  assert_frame_lists_equal(effect.getKeyframes(), [
+    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: null },
+    { offset: null, computedOffset: 0.5, easing: 'linear', composite: null },
+    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: null },
+  ]);
+}, 'An undefined keyframe returned from a custom iterator should be treated as a'
+    + ' default keyframe');
+
+test(() => {
+  const effect = new KeyframeEffect(null, createIterable([
+    { done: false, value: { left: '100px' } },
+    { done: false, value: null },
+    { done: false, value: { left: '200px' } },
+    { done: true },
+  ]));
+  assert_frame_lists_equal(effect.getKeyframes(), [
+    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: null },
+    { offset: null, computedOffset: 0.5, easing: 'linear', composite: null },
+    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: null },
+  ]);
+}, 'A null keyframe returned from a custom iterator should be treated as a'
+    + ' default keyframe');
+
+test(() => {
+  const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: ['100px', '200px'] } },
     { done: true },
   ]));
@@ -330,6 +374,44 @@
 }, 'A list of values returned from a custom iterator should be ignored');
 
 test(() => {
+  const test_error = { name: 'test' };
+  const keyframe_obj = {
+    [Symbol.iterator]() {
+      return { next() { throw test_error; } };
+    },
+  };
+  assert_throws(test_error, () => {
+    new KeyframeEffect(null, keyframe_obj);
+  });
+}, 'If a custom iterator throws from next(), the exception should be rethrown');
+
+// Test handling of invalid Symbol.iterator
+
+test(() => {
+  const test_error = { name: 'test' };
+  const keyframe_obj = {
+    [Symbol.iterator]() {
+      throw test_error;
+    },
+  };
+  assert_throws(test_error, () => {
+    new KeyframeEffect(null, keyframe_obj);
+  });
+}, 'Accessing a Symbol.iterator property that throws should rethrow');
+
+test(() => {
+  const keyframe_obj = {
+    [Symbol.iterator]() {
+      return 42;  // Not an object.
+    },
+  };
+  assert_throws({ name: 'TypeError' }, () => {
+    new KeyframeEffect(null, keyframe_obj);
+  });
+}, 'A non-object returned from the Symbol.iterator property should cause a'
+    + ' TypeError to be thrown');
+
+test(() => {
   const keyframe = {};
   Object.defineProperty(keyframe, 'width', { value: '200px' });
   Object.defineProperty(keyframe, 'height', {
diff --git a/web-animations/interfaces/KeyframeEffect/setKeyframes.html b/web-animations/interfaces/KeyframeEffect/setKeyframes.html
index 90ab89d..675a705 100644
--- a/web-animations/interfaces/KeyframeEffect/setKeyframes.html
+++ b/web-animations/interfaces/KeyframeEffect/setKeyframes.html
@@ -39,5 +39,17 @@
     });
   }, `KeyframeEffect constructor throws with ${subtest.desc}`);
 }
+
+test(t => {
+  const frames1 = [ { left: '100px' }, { left: '200px' } ];
+  const frames2 = [ { left: '200px' }, { left: '300px' } ];
+
+  const animation = target.animate(frames1, 1000);
+  animation.currentTime = 500;
+  assert_equals(getComputedStyle(target).left, "150px");
+
+  animation.effect.setKeyframes(frames2);
+  assert_equals(getComputedStyle(target).left, "250px");
+}, 'Changes made via setKeyframes should be immediately visible in style');
 </script>
 </body>
diff --git a/web-animations/testcommon.js b/web-animations/testcommon.js
index 46f2356..1a8ca79 100644
--- a/web-animations/testcommon.js
+++ b/web-animations/testcommon.js
@@ -88,7 +88,7 @@
 }
 
 // Create a pseudo element
-function createPseudo(test, type) {
+function getPseudoElement(test, type) {
   createStyle(test, { '@keyframes anim': '',
                       [`.pseudo::${type}`]: 'animation: anim 10s; ' +
                                             'content: \'\';'  });
@@ -97,8 +97,6 @@
   const anims = document.getAnimations();
   assert_true(anims.length >= 1);
   const anim = anims[anims.length - 1];
-  assert_equals(anim.effect.target.parentElement, div);
-  assert_equals(anim.effect.target.type, `::${type}`);
   anim.cancel();
   return anim.effect.target;
 }
diff --git a/webaudio/resources/audio-param.js b/webaudio/resources/audio-param.js
new file mode 100644
index 0000000..bc33fe8
--- /dev/null
+++ b/webaudio/resources/audio-param.js
@@ -0,0 +1,44 @@
+// Define functions that implement the formulas for AudioParam automations.
+
+// AudioParam linearRamp value at time t for a linear ramp between (t0, v0) and
+// (t1, v1).  It is assumed that t0 <= t.  Results are undefined otherwise.
+function audioParamLinearRamp(t, v0, t0, v1, t1) {
+  if (t >= t1)
+    return v1;
+  return (v0 + (v1 - v0) * (t - t0) / (t1 - t0))
+}
+
+// AudioParam exponentialRamp value at time t for an exponential ramp between
+// (t0, v0) and (t1, v1). It is assumed that t0 <= t.  Results are undefined
+// otherwise.
+function audioParamExponentialRamp(t, v0, t0, v1, t1) {
+  if (t >= t1)
+    return v1;
+  return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
+}
+
+// AudioParam setTarget value at time t for a setTarget curve starting at (t0,
+// v0) with a final value of vFainal and a time constant of timeConstant.  It is
+// assumed that t0 <= t.  Results are undefined otherwise.
+function audioParamSetTarget(t, v0, t0, vFinal, timeConstant) {
+  return vFinal + (v0 - vFinal) * Math.exp(-(t - t0) / timeConstant);
+}
+
+// AudioParam setValueCurve value at time t for a setValueCurve starting at time
+// t0 with curve, curve, and duration duration.  The sample rate is sampleRate.
+// It is assumed that t0 <= t.
+function audioParamSetValueCurve(t, curve, t0, duration) {
+  if (t > t0 + duration)
+    return curve[curve.length - 1];
+
+  let curvePointsPerSecond = (curve.length - 1) / duration;
+
+  let virtualIndex = (t - t0) * curvePointsPerSecond;
+  let index = Math.floor(virtualIndex);
+
+  let delta = virtualIndex - index;
+
+  let c0 = curve[index];
+  let c1 = curve[Math.min(index + 1, curve.length - 1)];
+  return c0 + (c1 - c0) * delta;
+}
diff --git a/webaudio/resources/audionodeoptions.js b/webaudio/resources/audionodeoptions.js
new file mode 100644
index 0000000..293e8e0
--- /dev/null
+++ b/webaudio/resources/audionodeoptions.js
@@ -0,0 +1,251 @@
+// Test that constructor for the node with name |nodeName| handles the
+// various possible values for channelCount, channelCountMode, and
+// channelInterpretation.
+
+// The |should| parameter is the test function from new |Audit|.
+function testAudioNodeOptions(should, context, nodeName, expectedNodeOptions) {
+  if (expectedNodeOptions === undefined)
+    expectedNodeOptions = {};
+  let node;
+
+  // Test that we can set channelCount and that errors are thrown for
+  // invalid values
+  let testChannelCount = 17;
+  if (expectedNodeOptions.channelCount) {
+    testChannelCount = expectedNodeOptions.channelCount.value;
+  }
+  should(
+      () => {
+        node = new window[nodeName](
+            context, Object.assign({}, expectedNodeOptions.additionalOptions, {
+              channelCount: testChannelCount
+            }));
+      },
+      'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '}}')
+      .notThrow();
+  should(node.channelCount, 'node.channelCount').beEqualTo(testChannelCount);
+
+  if (expectedNodeOptions.channelCount &&
+      expectedNodeOptions.channelCount.isFixed) {
+    // The channel count is fixed.  Verify that we throw an error if
+    // we try to change it. Arbitrarily set the count to be one more
+    // than the expected value.
+    testChannelCount = expectedNodeOptions.channelCount.value + 1;
+    should(
+        () => {
+          node = new window[nodeName](
+              context,
+              Object.assign(
+                  {}, expectedNodeOptions.additionalOptions,
+                  {channelCount: testChannelCount}));
+        },
+        'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '}}')
+        .throw(expectedNodeOptions.channelCount.errorType || 'TypeError');
+  } else {
+    // The channel count is not fixed.  Try to set the count to invalid
+    // values and make sure an error is thrown.
+    let errorType = 'NotSupportedError';
+
+    [0, 99].forEach(testValue => {
+      should(() => {
+        node = new window[nodeName](
+            context, Object.assign({}, expectedNodeOptions.additionalOptions, {
+              channelCount: testValue
+            }));
+      }, `new ${nodeName}(c, {channelCount: ${testValue}})`).throw(errorType);
+    });
+  }
+
+  // Test channelCountMode
+  let testChannelCountMode = 'max';
+  if (expectedNodeOptions.channelCountMode) {
+    testChannelCountMode = expectedNodeOptions.channelCountMode.value;
+  }
+  should(
+      () => {
+        node = new window[nodeName](
+            context, Object.assign({}, expectedNodeOptions.additionalOptions, {
+              channelCountMode: testChannelCountMode
+            }));
+      },
+      'new ' + nodeName + '(c, {channelCountMode: "' + testChannelCountMode +
+          '"}')
+      .notThrow();
+  should(node.channelCountMode, 'node.channelCountMode')
+      .beEqualTo(testChannelCountMode);
+
+  if (expectedNodeOptions.channelCountMode &&
+      expectedNodeOptions.channelCountMode.isFixed) {
+    // Channel count mode is fixed.  Test setting to something else throws.
+    ['max', 'clamped-max', 'explicit'].forEach(testValue => {
+      if (testValue !== expectedNodeOptions.channelCountMode.value) {
+        should(
+            () => {
+              node = new window[nodeName](
+                  context,
+                  Object.assign(
+                      {}, expectedNodeOptions.additionalOptions,
+                      {channelCountMode: testValue}));
+            },
+            `new ${nodeName}(c, {channelCountMode: "${testValue}"})`)
+            .throw(expectedNodeOptions.channelCountMode.errorType);
+      }
+    });
+  } else {
+    // Mode is not fixed. Verify that we can set the mode to all valid
+    // values, and that we throw for invalid values.
+
+    let testValues = ['max', 'clamped-max', 'explicit'];
+
+    testValues.forEach(testValue => {
+      should(() => {
+        node = new window[nodeName](
+            context, Object.assign({}, expectedNodeOptions.additionalOptions, {
+              channelCountMode: testValue
+            }));
+      }, `new ${nodeName}(c, {channelCountMode: "${testValue}"})`).notThrow();
+      should(
+          node.channelCountMode, 'node.channelCountMode after valid setter')
+          .beEqualTo(testValue);
+
+    });
+
+    should(
+        () => {
+          node = new window[nodeName](
+              context,
+              Object.assign(
+                  {}, expectedNodeOptions.additionalOptions,
+                  {channelCountMode: 'foobar'}));
+        },
+        'new ' + nodeName + '(c, {channelCountMode: "foobar"}')
+        .throw('TypeError');
+    should(node.channelCountMode, 'node.channelCountMode after invalid setter')
+        .beEqualTo(testValues[testValues.length - 1]);
+  }
+
+  // Test channelInterpretation
+  if (expectedNodeOptions.channelInterpretation &&
+      expectedNodeOptions.channelInterpretation.isFixed) {
+    // The channel interpretation is fixed.  Verify that we throw an
+    // error if we try to change it.
+    ['speakers', 'discrete'].forEach(testValue => {
+      if (testValue !== expectedNodeOptions.channelInterpretation.value) {
+        should(
+            () => {
+              node = new window[nodeName](
+                  context,
+                  Object.assign(
+                      {}, expectedNodeOptions.additionOptions,
+                      {channelInterpretation: testValue}));
+            },
+            `new ${nodeName}(c, {channelInterpretation: "${testValue}"})`)
+            .throw(expectedNodeOptions.channelInterpretation.errorType);
+      }
+    });
+  } else {
+    // Channel interpretation is not fixed. Verify that we can set it
+    // to all possible values.
+    should(
+        () => {
+          node = new window[nodeName](
+              context,
+              Object.assign(
+                  {}, expectedNodeOptions.additionalOptions,
+                  {channelInterpretation: 'speakers'}));
+        },
+        'new ' + nodeName + '(c, {channelInterpretation: "speakers"})')
+        .notThrow();
+    should(node.channelInterpretation, 'node.channelInterpretation')
+        .beEqualTo('speakers');
+
+    should(
+        () => {
+          node = new window[nodeName](
+              context,
+              Object.assign(
+                  {}, expectedNodeOptions.additionalOptions,
+                  {channelInterpretation: 'discrete'}));
+        },
+        'new ' + nodeName + '(c, {channelInterpretation: "discrete"})')
+        .notThrow();
+    should(node.channelInterpretation, 'node.channelInterpretation')
+        .beEqualTo('discrete');
+
+    should(
+        () => {
+          node = new window[nodeName](
+              context,
+              Object.assign(
+                  {}, expectedNodeOptions.additionalOptions,
+                  {channelInterpretation: 'foobar'}));
+        },
+        'new ' + nodeName + '(c, {channelInterpretation: "foobar"})')
+        .throw('TypeError');
+    should(
+        node.channelInterpretation,
+        'node.channelInterpretation after invalid setter')
+        .beEqualTo('discrete');
+  }
+}
+
+function initializeContext(should) {
+  let c;
+  should(() => {
+    c = new OfflineAudioContext(1, 1, 48000);
+  }, 'context = new OfflineAudioContext(...)').notThrow();
+
+  return c;
+}
+
+function testInvalidConstructor(should, name, context) {
+  should(() => {
+    new window[name]();
+  }, 'new ' + name + '()').throw('TypeError');
+  should(() => {
+    new window[name](1);
+  }, 'new ' + name + '(1)').throw('TypeError');
+  should(() => {
+    new window[name](context, 42);
+  }, 'new ' + name + '(context, 42)').throw('TypeError');
+}
+
+function testDefaultConstructor(should, name, context, options) {
+  let node;
+
+  let message = options.prefix + ' = new ' + name + '(context';
+  if (options.constructorOptions)
+    message += ', ' + JSON.stringify(options.constructorOptions);
+  message += ')'
+
+  should(() => {
+    node = new window[name](context, options.constructorOptions);
+  }, message).notThrow();
+
+  should(node instanceof window[name], options.prefix + ' instanceof ' + name)
+      .beEqualTo(true);
+  should(node.numberOfInputs, options.prefix + '.numberOfInputs')
+      .beEqualTo(options.numberOfInputs);
+  should(node.numberOfOutputs, options.prefix + '.numberOfOutputs')
+      .beEqualTo(options.numberOfOutputs);
+  should(node.channelCount, options.prefix + '.channelCount')
+      .beEqualTo(options.channelCount);
+  should(node.channelCountMode, options.prefix + '.channelCountMode')
+      .beEqualTo(options.channelCountMode);
+  should(node.channelInterpretation, options.prefix + '.channelInterpretation')
+      .beEqualTo(options.channelInterpretation);
+
+  return node;
+}
+
+function testDefaultAttributes(should, node, prefix, items) {
+  items.forEach((item) => {
+    let attr = node[item.name];
+    if (attr instanceof AudioParam) {
+      should(attr.value, prefix + '.' + item.name + '.value')
+          .beEqualTo(item.value);
+    } else {
+      should(attr, prefix + '.' + item.name).beEqualTo(item.value);
+    }
+  });
+}
diff --git a/webaudio/resources/biquad-testing.js b/webaudio/resources/biquad-testing.js
new file mode 100644
index 0000000..7a0b6e6
--- /dev/null
+++ b/webaudio/resources/biquad-testing.js
@@ -0,0 +1,171 @@
+// Globals, to make testing and debugging easier.
+let context;
+let filter;
+let signal;
+let renderedBuffer;
+let renderedData;
+
+let sampleRate = 44100.0;
+let pulseLengthFrames = .1 * sampleRate;
+
+// Maximum allowed error for the test to succeed.  Experimentally determined.
+let maxAllowedError = 5.9e-8;
+
+// This must be large enough so that the filtered result is
+// essentially zero.  See comments for createTestAndRun.
+let timeStep = .1;
+
+// Maximum number of filters we can process (mostly for setting the
+// render length correctly.)
+let maxFilters = 5;
+
+// How long to render.  Must be long enough for all of the filters we
+// want to test.
+let renderLengthSeconds = timeStep * (maxFilters + 1);
+
+let renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
+
+// Number of filters that will be processed.
+let nFilters;
+
+function createImpulseBuffer(context, length) {
+  let impulse = context.createBuffer(1, length, context.sampleRate);
+  let data = impulse.getChannelData(0);
+  for (let k = 1; k < data.length; ++k) {
+    data[k] = 0;
+  }
+  data[0] = 1;
+
+  return impulse;
+}
+
+
+function createTestAndRun(context, filterType, testParameters) {
+  // To test the filters, we apply a signal (an impulse) to each of
+  // the specified filters, with each signal starting at a different
+  // time.  The output of the filters is summed together at the
+  // output.  Thus for filter k, the signal input to the filter
+  // starts at time k * timeStep.  For this to work well, timeStep
+  // must be large enough for the output of each filter to have
+  // decayed to zero with timeStep seconds.  That way the filter
+  // outputs don't interfere with each other.
+
+  let filterParameters = testParameters.filterParameters;
+  nFilters = Math.min(filterParameters.length, maxFilters);
+
+  signal = new Array(nFilters);
+  filter = new Array(nFilters);
+
+  impulse = createImpulseBuffer(context, pulseLengthFrames);
+
+  // Create all of the signal sources and filters that we need.
+  for (let k = 0; k < nFilters; ++k) {
+    signal[k] = context.createBufferSource();
+    signal[k].buffer = impulse;
+
+    filter[k] = context.createBiquadFilter();
+    filter[k].type = filterType;
+    filter[k].frequency.value =
+        context.sampleRate / 2 * filterParameters[k].cutoff;
+    filter[k].detune.value = (filterParameters[k].detune === undefined) ?
+        0 :
+        filterParameters[k].detune;
+    filter[k].Q.value = filterParameters[k].q;
+    filter[k].gain.value = filterParameters[k].gain;
+
+    signal[k].connect(filter[k]);
+    filter[k].connect(context.destination);
+
+    signal[k].start(timeStep * k);
+  }
+
+  return context.startRendering().then(buffer => {
+    checkFilterResponse(buffer, filterType, testParameters);
+  });
+}
+
+function addSignal(dest, src, destOffset) {
+  // Add src to dest at the given dest offset.
+  for (let k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
+    dest[k] += src[j];
+  }
+}
+
+function generateReference(filterType, filterParameters) {
+  let result = new Array(renderLengthSamples);
+  let data = new Array(renderLengthSamples);
+  // Initialize the result array and data.
+  for (let k = 0; k < result.length; ++k) {
+    result[k] = 0;
+    data[k] = 0;
+  }
+  // Make data an impulse.
+  data[0] = 1;
+
+  for (let k = 0; k < nFilters; ++k) {
+    // Filter an impulse
+    let detune = (filterParameters[k].detune === undefined) ?
+        0 :
+        filterParameters[k].detune;
+    let frequency = filterParameters[k].cutoff *
+        Math.pow(2, detune / 1200);  // Apply detune, converting from Cents.
+
+    let filterCoef = createFilter(
+        filterType, frequency, filterParameters[k].q, filterParameters[k].gain);
+    let y = filterData(filterCoef, data, renderLengthSamples);
+
+    // Accumulate this filtered data into the final output at the desired
+    // offset.
+    addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate));
+  }
+
+  return result;
+}
+
+function checkFilterResponse(renderedBuffer, filterType, testParameters) {
+  let filterParameters = testParameters.filterParameters;
+  let maxAllowedError = testParameters.threshold;
+  let should = testParameters.should;
+
+  renderedData = renderedBuffer.getChannelData(0);
+
+  reference = generateReference(filterType, filterParameters);
+
+  let len = Math.min(renderedData.length, reference.length);
+
+  let success = true;
+
+  // Maximum error between rendered data and expected data
+  let maxError = 0;
+
+  // Sample offset where the maximum error occurred.
+  let maxPosition = 0;
+
+  // Number of infinities or NaNs that occurred in the rendered data.
+  let invalidNumberCount = 0;
+
+  should(nFilters, 'Number of filters tested')
+      .beEqualTo(filterParameters.length);
+
+  // Compare the rendered signal with our reference, keeping
+  // track of the maximum difference (and the offset of the max
+  // difference.)  Check for bad numbers in the rendered output
+  // too.  There shouldn't be any.
+  for (let k = 0; k < len; ++k) {
+    let err = Math.abs(renderedData[k] - reference[k]);
+    if (err > maxError) {
+      maxError = err;
+      maxPosition = k;
+    }
+    if (!isValidNumber(renderedData[k])) {
+      ++invalidNumberCount;
+    }
+  }
+
+  should(
+      invalidNumberCount, 'Number of non-finite values in the rendered output')
+      .beEqualTo(0);
+
+  should(maxError, 'Max error in ' + filterTypeName[filterType] + ' response')
+      .beLessThanOrEqualTo(maxAllowedError);
+}
diff --git a/webaudio/resources/mix-testing.js b/webaudio/resources/mix-testing.js
new file mode 100644
index 0000000..63c8e1a
--- /dev/null
+++ b/webaudio/resources/mix-testing.js
@@ -0,0 +1,23 @@
+let toneLengthSeconds = 1;
+
+// Create a buffer with multiple channels.
+// The signal frequency in each channel is the multiple of that in the first
+// channel.
+function createToneBuffer(context, frequency, duration, numberOfChannels) {
+  let sampleRate = context.sampleRate;
+  let sampleFrameLength = duration * sampleRate;
+
+  let audioBuffer =
+      context.createBuffer(numberOfChannels, sampleFrameLength, sampleRate);
+
+  let n = audioBuffer.length;
+
+  for (let k = 0; k < numberOfChannels; ++k) {
+    let data = audioBuffer.getChannelData(k);
+
+    for (let i = 0; i < n; ++i)
+      data[i] = Math.sin(frequency * (k + 1) * 2.0 * Math.PI * i / sampleRate);
+  }
+
+  return audioBuffer;
+}
diff --git a/webaudio/the-audio-api/the-analysernode-interface/ctor-analyser.html b/webaudio/the-audio-api/the-analysernode-interface/ctor-analyser.html
new file mode 100644
index 0000000..2112ede
--- /dev/null
+++ b/webaudio/the-audio-api/the-analysernode-interface/ctor-analyser.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: AnalyserNode
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'AnalyserNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'AnalyserNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 1,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'fftSize', value: 2048},
+          {name: 'frequencyBinCount', value: 1024},
+          {name: 'minDecibels', value: -100}, {name: 'maxDecibels', value: -30},
+          {name: 'smoothingTimeConstant', value: 0.8}
+        ]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'AnalyserNode');
+        task.done();
+      });
+
+      audit.define('constructor with options', (task, should) => {
+        let options = {
+          fftSize: 32,
+          maxDecibels: 1,
+          minDecibels: -13,
+          // Choose a value that can be represented the same as a float and as a
+          // double.
+          smoothingTimeConstant: 0.125
+        };
+
+        let node;
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node1 = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(node instanceof AnalyserNode, 'node1 instanceof AnalyserNode')
+            .beEqualTo(true);
+        should(node.fftSize, 'node1.fftSize').beEqualTo(options.fftSize);
+        should(node.maxDecibels, 'node1.maxDecibels')
+            .beEqualTo(options.maxDecibels);
+        should(node.minDecibels, 'node1.minDecibels')
+            .beEqualTo(options.minDecibels);
+        should(node.smoothingTimeConstant, 'node1.smoothingTimeConstant')
+            .beEqualTo(options.smoothingTimeConstant);
+
+        task.done();
+      });
+
+      audit.define('construct invalid options', (task, should) => {
+        let node;
+
+        should(
+            () => {
+              node = new AnalyserNode(context, {fftSize: 33});
+            },
+            'node = new AnalyserNode(c, { fftSize: 33 })')
+            .throw('IndexSizeError');
+        should(
+            () => {
+              node = new AnalyserNode(context, {maxDecibels: -500});
+            },
+            'node = new AnalyserNode(c, { maxDecibels: -500 })')
+            .throw('IndexSizeError');
+        should(
+            () => {
+              node = new AnalyserNode(context, {minDecibels: -10});
+            },
+            'node = new AnalyserNode(c, { minDecibels: -10 })')
+            .throw('IndexSizeError');
+        should(
+            () => {
+              node = new AnalyserNode(context, {smoothingTimeConstant: 2});
+            },
+            'node = new AnalyserNode(c, { smoothingTimeConstant: 2 })')
+            .throw('IndexSizeError');
+        should(function() {
+          node = new AnalyserNode(context, {frequencyBinCount: 33});
+        }, 'node = new AnalyserNode(c, { frequencyBinCount: 33 })').notThrow();
+        should(node.frequencyBinCount, 'node.frequencyBinCount')
+            .beEqualTo(1024);
+
+        task.done();
+      });
+
+      audit.define('setting min/max', (task, should) => {
+        let node;
+
+        // Recall the default values of minDecibels and maxDecibels are -100,
+        // and -30, respectively.  Setting both values in the constructor should
+        // not signal an error in any of the following cases.
+        let options = {minDecibels: -10, maxDecibels: 20};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        options = {maxDecibels: 20, minDecibels: -10};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        options = {minDecibels: -200, maxDecibels: -150};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        options = {maxDecibels: -150, minDecibels: -200};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        // But these should signal because minDecibel > maxDecibel
+        options = {maxDecibels: -150, minDecibels: -10};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .throw('IndexSizeError');
+
+        options = {minDecibels: -10, maxDecibels: -150};
+        should(
+            () => {
+              node = new AnalyserNode(context, options);
+            },
+            'node = new AnalyserNode(c, ' + JSON.stringify(options) + ')')
+            .throw('IndexSizeError');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audiobuffer-interface/ctor-audiobuffer.html b/webaudio/the-audio-api/the-audiobuffer-interface/ctor-audiobuffer.html
new file mode 100644
index 0000000..f6032f2
--- /dev/null
+++ b/webaudio/the-audio-api/the-audiobuffer-interface/ctor-audiobuffer.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: AudioBuffer
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        should(() => {
+          new AudioBuffer();
+        }, 'new AudioBuffer()').throw('TypeError');
+        should(() => {
+          new AudioBuffer(1);
+        }, 'new AudioBuffer(1)').throw('TypeError');
+        should(() => {
+          new AudioBuffer(Date, 42);
+        }, 'new AudioBuffer(Date, 42)').throw('TypeError');
+
+        task.done();
+      });
+
+      audit.define('required options', (task, should) => {
+        let buffer;
+
+        // The length and sampleRate attributes are required; all others are
+        // optional.
+        should(() => {
+          new AudioBuffer({});
+        }, 'buffer = new AudioBuffer({})').throw('TypeError');
+
+        should(() => {
+          new AudioBuffer({length: 1});
+        }, 'buffer = new AudioBuffer({length: 1})').throw('TypeError');
+
+        should(() => {
+          new AudioBuffer({sampleRate: 48000});
+        }, 'buffer = new AudioBuffer({sampleRate: 48000})').throw('TypeError');
+
+        should(() => {
+          buffer = new AudioBuffer({numberOfChannels: 1});
+        }, 'buffer = new AudioBuffer({numberOfChannels: 1}').throw('TypeError');
+
+        // Length and sampleRate are required, but others are optional.
+        should(
+            () => {
+              buffer =
+                  new AudioBuffer({length: 21, sampleRate: context.sampleRate});
+            },
+            'buffer0 = new AudioBuffer({length: 21, sampleRate: ' +
+                context.sampleRate + '}')
+            .notThrow();
+        // Verify the buffer has the correct values.
+        should(buffer.numberOfChannels, 'buffer0.numberOfChannels')
+            .beEqualTo(1);
+        should(buffer.length, 'buffer0.length').beEqualTo(21);
+        should(buffer.sampleRate, 'buffer0.sampleRate')
+            .beEqualTo(context.sampleRate);
+
+        should(
+            () => {
+              buffer = new AudioBuffer(
+                  {numberOfChannels: 3, length: 1, sampleRate: 48000});
+            },
+            'buffer1 = new AudioBuffer(' +
+                '{numberOfChannels: 3, length: 1, sampleRate: 48000})')
+            .notThrow();
+        // Verify the buffer has the correct values.
+        should(buffer.numberOfChannels, 'buffer1.numberOfChannels')
+            .beEqualTo(3);
+        should(buffer.length, 'buffer1.length').beEqualTo(1);
+        should(buffer.sampleRate, 'buffer1.sampleRate').beEqualTo(48000);
+
+        task.done();
+      });
+
+      audit.define('invalid option values', (task, should) => {
+        let options = {numberOfChannels: 0, length: 1, sampleRate: 16000};
+        should(
+            () => {
+              let buffer = new AudioBuffer(options);
+            },
+            'new AudioBuffer(' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {numberOfChannels: 99, length: 0, sampleRate: 16000};
+        should(
+            () => {
+              let buffer = new AudioBuffer(options);
+            },
+            'new AudioBuffer(' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {numberOfChannels: 1, length: 0, sampleRate: 16000};
+        should(
+            () => {
+              let buffer = new AudioBuffer(options);
+            },
+            'new AudioBuffer(' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {numberOfChannels: 1, length: 1, sampleRate: 100};
+        should(
+            () => {
+              let buffer = new AudioBuffer(options);
+            },
+            'new AudioBuffer(' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let buffer;
+
+        let options = {numberOfChannels: 5, length: 17, sampleRate: 16000};
+        should(
+            () => {
+              buffer = new AudioBuffer(options);
+            },
+            'buffer = new AudioBuffer(' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(buffer.numberOfChannels, 'buffer.numberOfChannels')
+            .beEqualTo(options.numberOfChannels);
+        should(buffer.length, 'buffer.length').beEqualTo(options.length);
+        should(buffer.sampleRate, 'buffer.sampleRate').beEqualTo(16000);
+
+        task.done();
+      });
+
+      audit.define('valid constructor', (task, should) => {
+        let buffer;
+
+        let options = {numberOfChannels: 3, length: 42, sampleRate: 54321};
+
+        let message = 'new AudioBuffer(' + JSON.stringify(options) + ')';
+        should(() => {
+          buffer = new AudioBuffer(options);
+        }, message).notThrow();
+
+        should(buffer.numberOfChannels, 'buffer.numberOfChannels')
+            .beEqualTo(options.numberOfChannels);
+
+        should(buffer.length, 'buffer.length').beEqualTo(options.length);
+
+        should(buffer.sampleRate, 'buffer.sampleRate')
+            .beEqualTo(options.sampleRate);
+
+        // Verify that we actually got the right number of channels
+        for (let k = 0; k < options.numberOfChannels; ++k) {
+          let data;
+          let message = 'buffer.getChannelData(' + k + ')';
+          should(() => {
+            data = buffer.getChannelData(k);
+          }, message).notThrow();
+
+          should(data.length, message + ' length').beEqualTo(options.length);
+        }
+
+        should(
+            () => {
+              buffer.getChannelData(options.numberOfChannels);
+            },
+            'buffer.getChannelData(' + options.numberOfChannels + ')')
+            .throw('IndexSizeError');
+
+        task.done();
+      });
+
+      audit.define('multiple contexts', (task, should) => {
+        // Test that an AudioBuffer can be used for different contexts.
+        let buffer =
+            new AudioBuffer({length: 128, sampleRate: context.sampleRate});
+
+        // Don't use getChannelData here because we want to be able to use
+        // |data| to compare the final results of playing out this buffer.  (If
+        // we did, |data| gets detached when the sources play.)
+        let data = new Float32Array(buffer.length);
+        for (let k = 0; k < data.length; ++k)
+          data[k] = 1 + k;
+        buffer.copyToChannel(data, 0);
+
+        let c1 = new OfflineAudioContext(1, 128, context.sampleRate);
+        let c2 = new OfflineAudioContext(1, 128, context.sampleRate);
+
+        let s1 = new AudioBufferSourceNode(c1, {buffer: buffer});
+        let s2 = new AudioBufferSourceNode(c2, {buffer: buffer});
+
+        s1.connect(c1.destination);
+        s2.connect(c2.destination);
+
+        s1.start();
+        s2.start();
+
+        Promise
+            .all([
+              c1.startRendering().then(function(resultBuffer) {
+                return should(resultBuffer.getChannelData(0), 'c1 result')
+                    .beEqualToArray(data);
+              }),
+              c2.startRendering().then(function(resultBuffer) {
+                return should(resultBuffer.getChannelData(0), 'c2 result')
+                    .beEqualToArray(data);
+              }),
+            ])
+            .then(returnValues => {
+              should(
+                  returnValues[0] && returnValues[1],
+                  'AudioBuffer shared between two different contexts')
+                  .message('correctly', 'incorrectly');
+              task.done();
+            });
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audiobuffersourcenode-interface/ctor-audiobuffersource.html b/webaudio/the-audio-api/the-audiobuffersourcenode-interface/ctor-audiobuffersource.html
new file mode 100644
index 0000000..c1c3203
--- /dev/null
+++ b/webaudio/the-audio-api/the-audiobuffersourcenode-interface/ctor-audiobuffersource.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: AudioBufferSource
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'AudioBufferSourceNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node =
+            testDefaultConstructor(should, 'AudioBufferSourceNode', context, {
+              prefix: prefix,
+              numberOfInputs: 0,
+              numberOfOutputs: 1,
+              channelCount: 2,
+              channelCountMode: 'max',
+              channelInterpretation: 'speakers'
+            });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'buffer', value: null},
+          {name: 'detune', value: 0},
+          {name: 'loop', value: false},
+          {name: 'loopEnd', value: 0.0},
+          {name: 'loopStart', value: 0.0},
+          {name: 'playbackRate', value: 1.0},
+        ]);
+
+        task.done();
+      });
+
+      audit.define('nullable buffer', (task, should) => {
+        let node;
+        let options = {buffer: null};
+
+        should(
+            () => {
+              node = new AudioBufferSourceNode(context, options);
+            },
+            'node1 = new AudioBufferSourceNode(c, ' + JSON.stringify(options))
+            .notThrow();
+
+        should(node.buffer, 'node1.buffer').beEqualTo(null);
+
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+        let buffer = context.createBuffer(2, 1000, context.sampleRate);
+
+        let options = {
+          buffer: buffer,
+          detune: .5,
+          loop: true,
+          loopEnd: (buffer.length / 2) / context.sampleRate,
+          loopStart: 5 / context.sampleRate,
+          playbackRate: .75
+        };
+
+        let message = 'node = new AudioBufferSourceNode(c, ' +
+            JSON.stringify(options) + ')';
+
+        should(() => {
+          node = new AudioBufferSourceNode(context, options);
+        }, message).notThrow();
+
+        // Use the factory method to create an equivalent node and compare the
+        // results from the constructor against this node.
+        let factoryNode = context.createBufferSource();
+        factoryNode.buffer = options.buffer;
+        factoryNode.detune.value = options.detune;
+        factoryNode.loop = options.loop;
+        factoryNode.loopEnd = options.loopEnd;
+        factoryNode.loopStart = options.loopStart;
+        factoryNode.playbackRate.value = options.playbackRate;
+
+        should(node.buffer === buffer, 'node2.buffer === buffer')
+            .beEqualTo(true);
+        should(node.detune.value, 'node2.detune.value')
+            .beEqualTo(factoryNode.detune.value);
+        should(node.loop, 'node2.loop').beEqualTo(factoryNode.loop);
+        should(node.loopEnd, 'node2.loopEnd').beEqualTo(factoryNode.loopEnd);
+        should(node.loopStart, 'node2.loopStart')
+            .beEqualTo(factoryNode.loopStart);
+        should(node.playbackRate.value, 'node2.playbackRate.value')
+            .beEqualTo(factoryNode.playbackRate.value);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-connect-audioratesignal.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-connect-audioratesignal.html
new file mode 100644
index 0000000..517d64f
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-connect-audioratesignal.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!-- 
+Tests that an audio-rate signal (AudioNode output) can be connected to an
+AudioParam.  Specifically, this tests that an audio-rate signal coming from an
+AudioBufferSourceNode playing an AudioBuffer containing a specific curve can be
+connected to an AudioGainNode's .gain attribute (an AudioParam).  Another
+AudioBufferSourceNode will be the audio source having its gain changed.  We load
+this one with an AudioBuffer containing a constant value of 1.  Thus it's easy
+to check that the resultant signal should be equal to the gain-scaling curve.
+-->
+<html>
+  <head>
+    <title>
+      audioparam-connect-audioratesignal.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      let sampleRate = 44100.0;
+      let lengthInSeconds = 1;
+
+      let context = 0;
+      let constantOneBuffer = 0;
+      let linearRampBuffer = 0;
+
+      function checkResult(renderedBuffer, should) {
+        let renderedData = renderedBuffer.getChannelData(0);
+        let expectedData = linearRampBuffer.getChannelData(0);
+        let n = renderedBuffer.length;
+
+        should(n, 'Rendered signal length').beEqualTo(linearRampBuffer.length);
+
+        // Check that the rendered result exactly matches the buffer used to
+        // control gain.  This is because we're changing the gain of a signal
+        // having constant value 1.
+        let success = true;
+        for (let i = 0; i < n; ++i) {
+          if (renderedData[i] != expectedData[i]) {
+            success = false;
+            break;
+          }
+        }
+
+        should(
+            success,
+            'Rendered signal exactly matches the audio-rate gain changing signal')
+            .beTrue();
+      }
+
+      audit.define('test', function(task, should) {
+        let sampleFrameLength = sampleRate * lengthInSeconds;
+
+        // Create offline audio context.
+        context = new OfflineAudioContext(1, sampleFrameLength, sampleRate);
+
+        // Create buffer used by the source which will have its gain controlled.
+        constantOneBuffer = createConstantBuffer(context, sampleFrameLength, 1);
+
+        // Create buffer used to control gain.
+        linearRampBuffer = createLinearRampBuffer(context, sampleFrameLength);
+
+        // Create the two sources.
+
+        let constantSource = context.createBufferSource();
+        constantSource.buffer = constantOneBuffer;
+
+        let gainChangingSource = context.createBufferSource();
+        gainChangingSource.buffer = linearRampBuffer;
+
+        // Create a gain node controlling the gain of constantSource and make
+        // the connections.
+        let gainNode = context.createGain();
+
+        // Intrinsic baseline gain of zero.
+        gainNode.gain.value = 0;
+
+        constantSource.connect(gainNode);
+        gainNode.connect(context.destination);
+
+        // Connect an audio-rate signal to control the .gain AudioParam.
+        // This is the heart of what is being tested.
+        gainChangingSource.connect(gainNode.gain);
+
+        // Start both sources at time 0.
+        constantSource.start(0);
+        gainChangingSource.start(0);
+
+        context.startRendering().then(buffer => {
+          checkResult(buffer, should);
+          task.done();
+        });
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-exceptional-values.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-exceptional-values.html
new file mode 100644
index 0000000..c2d18de
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-exceptional-values.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      audioparam-exceptional-values.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Context to use for all of the tests.  The context isn't used for any
+      // processing; just need one for creating a gain node, which is used for
+      // all the tests.
+      let context;
+
+      // For these values, AudioParam methods should throw a Typeerror because
+      // they are not finite values.
+      let nonFiniteValues = [Infinity, -Infinity, NaN];
+
+      audit.define('initialize', (task, should) => {
+        should(() => {
+          // Context for testing.  Rendering isn't done, so any valid values can
+          // be used here so might as well make them small.
+          context = new OfflineAudioContext(1, 1, 8000);
+        }, 'Creating context for testing').notThrow();
+
+        task.done();
+      });
+
+      audit.define(
+          {
+            label: 'test value',
+            description: 'Test non-finite arguments for AudioParam value'
+          },
+          (task, should) => {
+            let gain = context.createGain();
+
+            // Default method for generating the arguments for an automation
+            // method for testing the value of the automation.
+            let defaultFuncArg = (value) => [value, 1];
+
+            // Test the value parameter
+            doTests(should, gain, 'TypeError', nonFiniteValues, [
+              {automationName: 'setValueAtTime', funcArg: defaultFuncArg}, {
+                automationName: 'linearRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              {
+                automationName: 'exponentialRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              {
+                automationName: 'setTargetAtTime',
+                funcArg: (value) => [value, 1, 1]
+              }
+            ]);
+            task.done();
+          });
+
+      audit.define(
+          {
+            label: 'test time',
+            description: 'Test non-finite arguments for AudioParam time'
+          },
+          (task, should) => {
+            let gain = context.createGain();
+
+            // Default method for generating the arguments for an automation
+            // method for testing the time parameter of the automation.
+            let defaultFuncArg = (startTime) => [1, startTime];
+
+            // Test the time parameter
+            doTests(should, gain, 'TypeError', nonFiniteValues, [
+              {automationName: 'setValueAtTime', funcArg: defaultFuncArg},
+              {
+                automationName: 'linearRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              {
+                automationName: 'exponentialRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              // Test start time for setTarget
+              {
+                automationName: 'setTargetAtTime',
+                funcArg: (startTime) => [1, startTime, 1]
+              },
+              // Test time constant for setTarget
+              {
+                automationName: 'setTargetAtTime',
+                funcArg: (timeConstant) => [1, 1, timeConstant]
+              },
+            ]);
+
+            task.done();
+          });
+
+      audit.define(
+          {
+            label: 'test setValueCurve',
+            description: 'Test non-finite arguments for setValueCurveAtTime'
+          },
+          (task, should) => {
+            let gain = context.createGain();
+
+            // Just an array for use by setValueCurveAtTime. The length and
+            // contents of the array are not important.
+            let curve = new Float32Array(3);
+
+            doTests(should, gain, 'TypeError', nonFiniteValues, [
+              {
+                automationName: 'setValueCurveAtTime',
+                funcArg: (startTime) => [curve, startTime, 1]
+              },
+            ]);
+
+            // Non-finite values for the curve should signal an error
+            doTests(
+                should, gain, 'TypeError',
+                [[1, 2, Infinity, 3], [1, NaN, 2, 3]], [{
+                  automationName: 'setValueCurveAtTime',
+                  funcArg: (c) => [c, 1, 1]
+                }]);
+
+            task.done();
+          });
+
+      audit.define(
+          {
+            label: 'special cases 1',
+            description: 'Test exceptions for finite values'
+          },
+          (task, should) => {
+            let gain = context.createGain();
+
+            // Default method for generating the arguments for an automation
+            // method for testing the time parameter of the automation.
+            let defaultFuncArg = (startTime) => [1, startTime];
+
+            // Test the time parameter
+            let curve = new Float32Array(3);
+            doTests(should, gain, 'RangeError', [-1], [
+              {automationName: 'setValueAtTime', funcArg: defaultFuncArg},
+              {
+                automationName: 'linearRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              {
+                automationName: 'exponentialRampToValueAtTime',
+                funcArg: defaultFuncArg
+              },
+              {
+                automationName: 'setTargetAtTime',
+                funcArg: (startTime) => [1, startTime, 1]
+              },
+              // Test time constant
+              {
+                automationName: 'setTargetAtTime',
+                funcArg: (timeConstant) => [1, 1, timeConstant]
+              },
+              // startTime and duration for setValueCurve
+              {
+                automationName: 'setValueCurveAtTime',
+                funcArg: (startTime) => [curve, startTime, 1]
+              },
+              {
+                automationName: 'setValueCurveAtTime',
+                funcArg: (duration) => [curve, 1, duration]
+              },
+            ]);
+
+            // One final test for setValueCurve: duration can't be 0.
+            should(
+                () => gain.gain.setValueCurveAtTime(curve, 1, 0),
+                'gain.gain.setValueCurveAtTime(curve, 1, 0)')
+                .throw('RangeError');
+
+            task.done();
+          });
+
+      audit.define(
+          {
+            label: 'special cases 2',
+            description: 'Test special cases for expeonentialRamp'
+          },
+          (task, should) => {
+            let gain = context.createGain();
+
+            doTests(should, gain, 'RangeError', [0, -1e-100, 1e-100], [{
+                      automationName: 'exponentialRampToValueAtTime',
+                      funcArg: (value) => [value, 1]
+                    }]);
+
+            task.done();
+          });
+
+      audit.run();
+
+      // Run test over the set of values in |testValues| for all of the
+      // automation methods in |testMethods|.  The expected error type is
+      // |errorName|. |testMethods| is an array of dictionaries with attributes
+      // |automationName| giving the name of the automation method to be tested
+      // and |funcArg| being a function of one parameter that produces an array
+      // that will be used as the argument to the automation method.
+      function doTests(should, node, errorName, testValues, testMethods) {
+        testValues.forEach(value => {
+          testMethods.forEach(method => {
+            let args = method.funcArg(value);
+            let message = 'gain.gain.' + method.automationName + '(' +
+                argString(args) + ')';
+            should(() => node.gain[method.automationName](...args), message)
+                .throw(errorName);
+          });
+        });
+      }
+
+      // Specialized printer for automation arguments so that messages make
+      // sense.  We assume the first element is either a number or an array.  If
+      // it's an array, there are always three elements, and we want to print
+      // out the brackets for the array argument.
+      function argString(arg) {
+        if (typeof(arg[0]) === 'number') {
+          return arg.toString();
+        }
+
+        return '[' + arg[0] + '],' + arg[1] + ',' + arg[2];
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-exponentialRampToValueAtTime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-exponentialRampToValueAtTime.html
new file mode 100644
index 0000000..bec4c12
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-exponentialRampToValueAtTime.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test AudioParam.exponentialRampToValueAtTime
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Play a long DC signal out through an AudioGainNode, and call
+      // setValueAtTime() and exponentialRampToValueAtTime() at regular
+      // intervals to set the starting and ending values for an exponential
+      // ramp.  Each time interval has a ramp with a different starting and
+      // ending value so that there is a discontinuity at each time interval
+      // boundary.  The discontinuity is for testing timing.  Also, we alternate
+      // between an increasing and decreasing ramp for each interval.
+
+      // Number of tests to run.
+      let numberOfTests = 100;
+
+      // Max allowed difference between the rendered data and the expected
+      // result.
+      let maxAllowedError = 1.222e-5;
+
+      // The AudioGainNode starts with this value instead of the default value.
+      let initialValue = 100;
+
+      // Set the gain node value to the specified value at the specified time.
+      function setValue(value, time) {
+        gainNode.gain.setValueAtTime(value, time);
+      }
+
+      // Generate an exponential ramp ending at time |endTime| with an ending
+      // value of |value|.
+      function generateRamp(value, startTime, endTime){
+          // |startTime| is ignored because the exponential ramp
+          // uses the value from the setValueAtTime() call above.
+          gainNode.gain.exponentialRampToValueAtTime(value, endTime)}
+
+      audit.define(
+          {
+            label: 'test',
+            description:
+                'AudioParam exponentialRampToValueAtTime() functionality'
+          },
+          function(task, should) {
+            createAudioGraphAndTest(
+                task, should, numberOfTests, initialValue, setValue,
+                generateRamp, 'exponentialRampToValueAtTime()', maxAllowedError,
+                createExponentialRampArray);
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-large-endtime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-large-endtime.html
new file mode 100644
index 0000000..d8f38ee
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-large-endtime.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      AudioParam with Huge End Time
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let sampleRate = 48000;
+      // Render for some small (but fairly arbitrary) time.
+      let renderDuration = 0.125;
+      // Any huge time value that won't fit in a size_t (2^64 on a 64-bit
+      // machine).
+      let largeTime = 1e300;
+
+      let audit = Audit.createTaskRunner();
+
+      // See crbug.com/582701.  Create an audioparam with a huge end time and
+      // verify that to automation is run.  We don't care about the actual
+      // results, just that it runs.
+
+      // Test linear ramp with huge end time
+      audit.define('linearRamp', (task, should) => {
+        let graph = createGraph();
+        graph.gain.gain.linearRampToValueAtTime(0.1, largeTime);
+
+        graph.source.start();
+        graph.context.startRendering()
+            .then(function(buffer) {
+              should(true, 'linearRampToValue(0.1, ' + largeTime + ')')
+                  .message('successfully rendered', 'unsuccessfully rendered');
+            })
+            .then(() => task.done());
+      });
+
+      // Test exponential ramp with huge end time
+      audit.define('exponentialRamp', (task, should) => {
+        let graph = createGraph();
+        graph.gain.gain.exponentialRampToValueAtTime(.1, largeTime);
+
+        graph.source.start();
+        graph.context.startRendering()
+            .then(function(buffer) {
+              should(true, 'exponentialRampToValue(0.1, ' + largeTime + ')')
+                  .message('successfully rendered', 'unsuccessfully rendered');
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+
+      // Create the graph and return the context, the source, and the gain node.
+      function createGraph() {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+        let src = context.createBufferSource();
+        src.buffer = createConstantBuffer(context, 1, 1);
+        src.loop = true;
+        let gain = context.createGain();
+        src.connect(gain);
+        gain.connect(context.destination);
+        gain.gain.setValueAtTime(1, 0.1 / sampleRate);
+
+        return {context: context, gain: gain, source: src};
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-linearRampToValueAtTime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-linearRampToValueAtTime.html
new file mode 100644
index 0000000..509c254
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-linearRampToValueAtTime.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test AudioParam.linearRampToValueAtTime
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Play a long DC signal out through an AudioGainNode, and call
+      // setValueAtTime() and linearRampToValueAtTime() at regular intervals to
+      // set the starting and ending values for a linear ramp. Each time
+      // interval has a ramp with a different starting and ending value so that
+      // there is a discontinuity at each time interval boundary.  The
+      // discontinuity is for testing timing.  Also, we alternate between an
+      // increasing and decreasing ramp for each interval.
+
+      // Number of tests to run.
+      let numberOfTests = 100;
+
+      // Max allowed difference between the rendered data and the expected
+      // result.
+      let maxAllowedError = 1.865e-6;
+
+      // Set the gain node value to the specified value at the specified time.
+      function setValue(value, time) {
+        gainNode.gain.setValueAtTime(value, time);
+      }
+
+      // Generate a linear ramp ending at time |endTime| with an ending value of
+      // |value|.
+      function generateRamp(value, startTime, endTime){
+          // |startTime| is ignored because the linear ramp uses the value from
+          // the
+          // setValueAtTime() call above.
+          gainNode.gain.linearRampToValueAtTime(value, endTime)}
+
+      audit.define(
+          {
+            label: 'test',
+            description: 'AudioParam linearRampToValueAtTime() functionality'
+          },
+          function(task, should) {
+            createAudioGraphAndTest(
+                task, should, numberOfTests, 1, setValue, generateRamp,
+                'linearRampToValueAtTime()', maxAllowedError,
+                createLinearRampArray);
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-method-chaining.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-method-chaining.html
new file mode 100644
index 0000000..ed25a01
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-method-chaining.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      audioparam-method-chaining.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let sampleRate = 8000;
+
+      // Create a dummy array for setValueCurveAtTime method.
+      let curveArray = new Float32Array([5.0, 6.0]);
+
+      // AudioNode dictionary with associated dummy arguments.
+      let methodDictionary = [
+        {name: 'setValueAtTime', args: [1.0, 0.0]},
+        {name: 'linearRampToValueAtTime', args: [2.0, 1.0]},
+        {name: 'exponentialRampToValueAtTime', args: [3.0, 2.0]},
+        {name: 'setTargetAtTime', args: [4.0, 2.0, 0.5]},
+        {name: 'setValueCurveAtTime', args: [curveArray, 5.0, 1.0]},
+        {name: 'cancelScheduledValues', args: [6.0]}
+      ];
+
+      let audit = Audit.createTaskRunner();
+
+      // Task: testing entries from the dictionary.
+      audit.define('from-dictionary', (task, should) => {
+        let context = new AudioContext();
+
+        methodDictionary.forEach(function(method) {
+          let sourceParam = context.createGain().gain;
+          should(
+              sourceParam === sourceParam[method.name](...method.args),
+              'The return value of ' + sourceParam.constructor.name + '.' +
+                  method.name + '()' +
+                  ' matches the source AudioParam')
+              .beEqualTo(true);
+
+        });
+
+        task.done();
+      });
+
+      // Task: test method chaining with invalid operation.
+      audit.define('invalid-operation', (task, should) => {
+        let context = new OfflineAudioContext(1, sampleRate, sampleRate);
+        let osc = context.createOscillator();
+        let amp1 = context.createGain();
+        let amp2 = context.createGain();
+
+        osc.connect(amp1);
+        osc.connect(amp2);
+        amp1.connect(context.destination);
+        amp2.connect(context.destination);
+
+        // The first operation fails with an exception, thus the second one
+        // should not have effect on the parameter value. Instead, it should
+        // maintain the default value of 1.0.
+        should(
+            function() {
+              amp1.gain.setValueAtTime(0.25, -1.0)
+                  .linearRampToValueAtTime(2.0, 1.0);
+            },
+            'Calling setValueAtTime() with a negative end time')
+            .throw('RangeError');
+
+        // The first operation succeeds but the second fails due to zero target
+        // value for the exponential ramp. Thus only the first should have
+        // effect on the parameter value, setting the value to 0.5.
+        should(
+            function() {
+              amp2.gain.setValueAtTime(0.5, 0.0).exponentialRampToValueAtTime(
+                  0.0, 1.0);
+            },
+            'Calling exponentialRampToValueAtTime() with a zero target value')
+            .throw('RangeError');
+
+        osc.start();
+        osc.stop(1.0);
+
+        context.startRendering()
+            .then(function(buffer) {
+              should(amp1.gain.value, 'The gain value of the first gain node')
+                  .beEqualTo(1.0);
+              should(amp2.gain.value, 'The gain value of the second gain node')
+                  .beEqualTo(0.5);
+            })
+            .then(() => task.done());
+      });
+
+      // Task: verify if the method chaining actually works. Create an arbitrary
+      // envelope and compare the result with the expected one created by JS
+      // code.
+      audit.define('verification', (task, should) => {
+        let context = new OfflineAudioContext(1, sampleRate * 4, sampleRate);
+        let constantBuffer = createConstantBuffer(context, 1, 1.0);
+
+        let source = context.createBufferSource();
+        source.buffer = constantBuffer;
+        source.loop = true;
+
+        let envelope = context.createGain();
+
+        source.connect(envelope);
+        envelope.connect(context.destination);
+
+        envelope.gain.setValueAtTime(0.0, 0.0)
+            .linearRampToValueAtTime(1.0, 1.0)
+            .exponentialRampToValueAtTime(0.5, 2.0)
+            .setTargetAtTime(0.001, 2.0, 0.5);
+
+        source.start();
+
+        context.startRendering()
+            .then(function(buffer) {
+              let expectedEnvelope =
+                  createLinearRampArray(0.0, 1.0, 0.0, 1.0, sampleRate);
+              expectedEnvelope.push(...createExponentialRampArray(
+                  1.0, 2.0, 1.0, 0.5, sampleRate));
+              expectedEnvelope.push(...createExponentialApproachArray(
+                  2.0, 4.0, 0.5, 0.001, sampleRate, 0.5));
+
+              // There are slight differences between JS implementation of
+              // AudioParam envelope and the internal implementation. (i.e.
+              // double/float and rounding up) The error threshold is adjusted
+              // empirically through the local testing.
+              should(buffer.getChannelData(0), 'The rendered envelope')
+                  .beCloseToArray(
+                      expectedEnvelope, {absoluteThreshold: 4.0532e-6});
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-setTargetAtTime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setTargetAtTime.html
new file mode 100644
index 0000000..faf00c0
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setTargetAtTime.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test AudioParam.setTargetAtTime
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Play a long DC signal out through an AudioGainNode, and call
+      // setValueAtTime() and setTargetAtTime at regular intervals to set the
+      // starting value and the target value. Each time interval has a ramp with
+      // a different starting and target value so that there is a discontinuity
+      // at each time interval boundary. The discontinuity is for testing
+      // timing.  Also, we alternate between an increasing and decreasing ramp
+      // for each interval.
+
+      // Number of tests to run.
+      let numberOfTests = 100;
+
+      // Max allowed difference between the rendered data and the expected
+      // result.
+      let maxAllowedError = 6.5683e-4
+
+      // The AudioGainNode starts with this value instead of the default value.
+      let initialValue = 100;
+
+      // Set the gain node value to the specified value at the specified time.
+      function setValue(value, time) {
+        gainNode.gain.setValueAtTime(value, time);
+      }
+
+      // Generate an exponential approach starting at |startTime| with a target
+      // value of |value|.
+      function automation(value, startTime, endTime){
+          // endTime is not used for setTargetAtTime.
+          gainNode.gain.setTargetAtTime(value, startTime, timeConstant)}
+
+      audit.define(
+          {
+            label: 'test',
+            description: 'AudioParam setTargetAtTime() functionality.'
+          },
+          function(task, should) {
+            createAudioGraphAndTest(
+                task, should, numberOfTests, initialValue, setValue, automation,
+                'setTargetAtTime()', maxAllowedError,
+                createExponentialApproachArray);
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueAtTime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueAtTime.html
new file mode 100644
index 0000000..ab2edfd
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueAtTime.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      audioparam-setValueAtTime.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Play a long DC signal out through an AudioGainNode, and call
+      // setValueAtTime() at regular intervals to set the value for the duration
+      // of the interval.  Each time interval has different value so that there
+      // is a discontinuity at each time interval boundary.  The discontinuity
+      // is for testing timing.
+
+      // Number of tests to run.
+      let numberOfTests = 100;
+
+      // Max allowed difference between the rendered data and the expected
+      // result.
+      let maxAllowedError = 6e-8;
+
+      // Set the gain node value to the specified value at the specified time.
+      function setValue(value, time) {
+        gainNode.gain.setValueAtTime(value, time);
+      }
+
+      // For testing setValueAtTime(), we don't need to do anything for
+      // automation. because the value at the beginning of the interval is set
+      // by setValue and it remains constant for the duration, which is what we
+      // want.
+      function automation(value, startTime, endTime) {
+        // Do nothing.
+      }
+
+      audit.define(
+          {
+            label: 'test',
+            description: 'AudioParam setValueAtTime() functionality.'
+          },
+          function(task, should) {
+            createAudioGraphAndTest(
+                task, should, numberOfTests, 1, setValue, automation,
+                'setValueAtTime()', maxAllowedError, createConstantArray);
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurve-exceptions.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurve-exceptions.html
new file mode 100644
index 0000000..590dcbd
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurve-exceptions.html
@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Exceptions from setValueCurveAtTime
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let sampleRate = 48000;
+      // Some short duration because we don't need to run the test for very
+      // long.
+      let testDurationSec = 0.125;
+      let testDurationFrames = testDurationSec * sampleRate;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('setValueCurve', (task, should) => {
+        let success = true;
+        let context =
+            new OfflineAudioContext(1, testDurationFrames, sampleRate);
+        let g = context.createGain();
+        let curve = new Float32Array(2);
+
+        // Start time and duration for setValueCurveAtTime
+        let curveStartTime = 0.1 * testDurationSec;
+        let duration = 0.1 * testDurationSec;
+
+        // Some time that is known to during the setValueCurveTime interval.
+        let automationTime = curveStartTime + duration / 2;
+
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, curveStartTime, duration);
+            },
+            'setValueCurveAtTime(curve, ' + curveStartTime + ', ' + duration +
+                ')')
+            .notThrow();
+
+        should(
+            function() {
+              g.gain.setValueAtTime(1, automationTime);
+            },
+            'setValueAtTime(1, ' + automationTime + ')')
+            .throw('NotSupportedError');
+
+        should(
+            function() {
+              g.gain.linearRampToValueAtTime(1, automationTime);
+            },
+            'linearRampToValueAtTime(1, ' + automationTime + ')')
+            .throw('NotSupportedError');
+
+        should(
+            function() {
+              g.gain.exponentialRampToValueAtTime(1, automationTime);
+            },
+            'exponentialRampToValueAtTime(1, ' + automationTime + ')')
+            .throw('NotSupportedError');
+
+        should(
+            function() {
+              g.gain.setTargetAtTime(1, automationTime, 1);
+            },
+            'setTargetAtTime(1, ' + automationTime + ', 1)')
+            .throw('NotSupportedError');
+
+        should(
+            function() {
+              g.gain.setValueAtTime(1, curveStartTime + 1.1 * duration);
+            },
+            'setValueAtTime(1, ' + (curveStartTime + 1.1 * duration) + ')')
+            .notThrow();
+
+        task.done();
+      });
+
+      audit.define('automations', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, testDurationFrames, sampleRate);
+        let g = context.createGain();
+
+        let curve = new Float32Array(2);
+        // Start time and duration for setValueCurveAtTime
+        let startTime = 0;
+        let timeInterval = testDurationSec / 10;
+        let time;
+
+        startTime += timeInterval;
+        should(() => {
+          g.gain.linearRampToValueAtTime(1, startTime);
+        }, 'linearRampToValueAtTime(1, ' + startTime + ')').notThrow();
+
+        startTime += timeInterval;
+        should(() => {
+          g.gain.exponentialRampToValueAtTime(1, startTime);
+        }, 'exponentialRampToValueAtTime(1, ' + startTime + ')').notThrow();
+
+        startTime += timeInterval;
+        should(() => {
+          g.gain.setTargetAtTime(1, startTime, 0.1);
+        }, 'setTargetAtTime(1, ' + startTime + ', 0.1)').notThrow();
+
+        startTime += timeInterval;
+        should(() => {
+          g.gain.setValueCurveAtTime(curve, startTime, 0.1);
+        }, 'setValueCurveAtTime(curve, ' + startTime + ', 0.1)').notThrow();
+
+        // Now try to setValueCurve that overlaps each of the above automations
+        startTime = timeInterval / 2;
+
+        for (let k = 0; k < 4; ++k) {
+          time = startTime + timeInterval * k;
+          should(
+              () => {
+                g.gain.setValueCurveAtTime(curve, time, 0.01);
+              },
+              'setValueCurveAtTime(curve, ' + time + ', 0.01)')
+              .throw('NotSupportedError');
+        }
+
+        // Elements of setValueCurve should be finite.
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(
+                  Float32Array.from([NaN, NaN]), time, 0.01);
+            },
+            'setValueCurveAtTime([NaN, NaN], ' + time + ', 0.01)')
+            .throw('TypeError');
+
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(
+                  Float32Array.from([1, Infinity]), time, 0.01);
+            },
+            'setValueCurveAtTime([1, Infinity], ' + time + ', 0.01)')
+            .throw('TypeError');
+
+        let d = context.createDelay();
+        // Check that we get warnings for out-of-range values and also throw for
+        // non-finite values.
+        should(
+            () => {
+              d.delayTime.setValueCurveAtTime(
+                  Float32Array.from([1, 5]), time, 0.01);
+            },
+            'delayTime.setValueCurveAtTime([1, 5], ' + time + ', 0.01)')
+            .notThrow();
+
+        should(
+            () => {
+              d.delayTime.setValueCurveAtTime(
+                  Float32Array.from([1, 5, Infinity]), time, 0.01);
+            },
+            'delayTime.setValueCurveAtTime([1, 5, Infinity], ' + time +
+                ', 0.01)')
+            .throw('TypeError');
+
+        // One last test that prints out lots of digits for the time.
+        time = Math.PI / 100;
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, time, 0.01);
+            },
+            'setValueCurveAtTime(curve, ' + time + ', 0.01)')
+            .throw('NotSupportedError');
+
+        task.done();
+      });
+
+      audit.define('catch-exception', (task, should) => {
+        // Verify that the curve isn't inserted into the time line even if we
+        // catch the exception.
+        let success = true;
+        let context =
+            new OfflineAudioContext(1, testDurationFrames, sampleRate);
+        let gain = context.createGain();
+        let source = context.createBufferSource();
+        let buffer = context.createBuffer(1, 1, context.sampleRate);
+        buffer.getChannelData(0)[0] = 1;
+        source.buffer = buffer;
+        source.loop = true;
+
+        source.connect(gain);
+        gain.connect(context.destination);
+
+        gain.gain.setValueAtTime(1, 0);
+        try {
+          // The value curve has an invalid element. This automation shouldn't
+          // be inserted into the timeline at all.
+          gain.gain.setValueCurveAtTime(
+              Float32Array.from([0, NaN]), 128 / context.sampleRate, .5);
+        } catch (e) {
+        };
+        source.start();
+
+        context.startRendering()
+            .then(function(resultBuffer) {
+              // Since the setValueCurve wasn't inserted, the output should be
+              // exactly 1 for the entire duration.
+              should(
+                  resultBuffer.getChannelData(0),
+                  'Handled setValueCurve exception so output')
+                  .beConstantValueOf(1);
+
+            })
+            .then(() => task.done());
+      });
+
+      audit.define('start-end', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, testDurationFrames, sampleRate);
+        let g = context.createGain();
+        let curve = new Float32Array(2);
+
+        // Verify that a setValueCurve can start at the end of an automation.
+        let time = 0;
+        let timeInterval = testDurationSec / 50;
+        should(() => {
+          g.gain.setValueAtTime(1, time);
+        }, 'setValueAtTime(1, ' + time + ')').notThrow();
+
+        time += timeInterval;
+        should(() => {
+          g.gain.linearRampToValueAtTime(0, time);
+        }, 'linearRampToValueAtTime(0, ' + time + ')').notThrow();
+
+        // setValueCurve starts at the end of the linear ramp. This should be
+        // fine.
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, time, timeInterval);
+            },
+            'setValueCurveAtTime(..., ' + time + ', ' + timeInterval + ')')
+            .notThrow();
+
+        // exponentialRamp ending one interval past the setValueCurve should be
+        // fine.
+        time += 2 * timeInterval;
+        should(() => {
+          g.gain.exponentialRampToValueAtTime(1, time);
+        }, 'exponentialRampToValueAtTime(1, ' + time + ')').notThrow();
+
+        // setValueCurve starts at the end of the exponential ramp. This should
+        // be fine.
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, time, timeInterval);
+            },
+            'setValueCurveAtTime(..., ' + time + ', ' + timeInterval + ')')
+            .notThrow();
+
+        // setValueCurve at the end of the setValueCurve should be fine.
+        time += timeInterval;
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, time, timeInterval);
+            },
+            'setValueCurveAtTime(..., ' + time + ', ' + timeInterval + ')')
+            .notThrow();
+
+        // setValueAtTime at the end of setValueCurve should be fine.
+        time += timeInterval;
+        should(() => {
+          g.gain.setValueAtTime(0, time);
+        }, 'setValueAtTime(0, ' + time + ')').notThrow();
+
+        // setValueCurve at the end of setValueAtTime should be fine.
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(curve, time, timeInterval);
+            },
+            'setValueCurveAtTime(..., ' + time + ', ' + timeInterval + ')')
+            .notThrow();
+
+        // setTarget starting at the end of setValueCurve should be fine.
+        time += timeInterval;
+        should(() => {
+          g.gain.setTargetAtTime(1, time, 1);
+        }, 'setTargetAtTime(1, ' + time + ', 1)').notThrow();
+
+        task.done();
+      });
+
+      audit.define('curve lengths', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, testDurationFrames, sampleRate);
+        let g = context.createGain();
+        let time = 0;
+
+        // Check for invalid curve lengths
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(Float32Array.from([]), time, 0.01);
+            },
+            'setValueCurveAtTime([], ' + time + ', 0.01)')
+            .throw('InvalidStateError');
+
+        should(
+            () => {
+              g.gain.setValueCurveAtTime(Float32Array.from([1]), time, 0.01);
+            },
+            'setValueCurveAtTime([1], ' + time + ', 0.01)')
+            .throw('InvalidStateError');
+
+        should(() => {
+          g.gain.setValueCurveAtTime(Float32Array.from([1, 2]), time, 0.01);
+        }, 'setValueCurveAtTime([1,2], ' + time + ', 0.01)').notThrow();
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurveAtTime.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurveAtTime.html
new file mode 100644
index 0000000..de84062
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-setValueCurveAtTime.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test AudioParam.setValueCurveAtTime
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Play a long DC signal out through an AudioGainNode and for each time
+      // interval call setValueCurveAtTime() to set the values for the duration
+      // of the interval.  Each curve is a sine wave, and we assume that the
+      // time interval is not an exact multiple of the period. This causes a
+      // discontinuity between time intervals which is used to test timing.
+
+      // Number of tests to run.
+      let numberOfTests = 20;
+
+      // Max allowed difference between the rendered data and the expected
+      // result. Because of the linear interpolation, the rendered curve isn't
+      // exactly the same as the reference.  This value is experimentally
+      // determined.
+      let maxAllowedError = 3.7194e-6;
+
+      // The amplitude of the sine wave.
+      let sineAmplitude = 1;
+
+      // Frequency of the sine wave.
+      let freqHz = 440;
+
+      // Curve to use for setValueCurveAtTime().
+      let curve;
+
+      // Sets the curve data for the entire time interval.
+      function automation(value, startTime, endTime) {
+        gainNode.gain.setValueCurveAtTime(
+            curve, startTime, endTime - startTime);
+      }
+
+      audit.define(
+          {
+            label: 'test',
+            description: 'AudioParam setValueCurveAtTime() functionality.'
+          },
+          function(task, should) {
+            // The curve of values to use.
+            curve = createSineWaveArray(
+                timeInterval, freqHz, sineAmplitude, sampleRate);
+
+            createAudioGraphAndTest(
+                task, should, numberOfTests, sineAmplitude,
+                function(k) {
+                  // Don't need to set the value.
+                },
+                automation, 'setValueCurveAtTime()', maxAllowedError,
+                createReferenceSineArray,
+                2 * Math.PI * sineAmplitude * freqHz / sampleRate,
+                differenceErrorMetric);
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/audioparam-summingjunction.html b/webaudio/the-audio-api/the-audioparam-interface/audioparam-summingjunction.html
new file mode 100644
index 0000000..9084942
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/audioparam-summingjunction.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!--
+Tests that multiple audio-rate signals (AudioNode outputs) can be connected to an AudioParam
+and that these signals are summed, along with the AudioParams intrinsic value.
+-->
+<html>
+  <head>
+    <title>
+      audioparam-summingjunction.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/mix-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      let sampleRate = 44100.0;
+      let lengthInSeconds = 1;
+
+      let context = 0;
+
+      // Buffers used by the two gain controlling sources.
+      let linearRampBuffer;
+      let toneBuffer;
+      let toneFrequency = 440;
+
+      // Arbitrary non-zero value.
+      let baselineGain = 5;
+
+      // Allow for a small round-off error.
+      let maxAllowedError = 1e-6;
+
+      function checkResult(renderedBuffer, should) {
+        let renderedData = renderedBuffer.getChannelData(0);
+
+        // Get buffer data from the two sources used to control gain.
+        let linearRampData = linearRampBuffer.getChannelData(0);
+        let toneData = toneBuffer.getChannelData(0);
+
+        let n = renderedBuffer.length;
+
+        should(n, 'Rendered signal length').beEqualTo(linearRampBuffer.length);
+
+        // Check that the rendered result exactly matches the sum of the
+        // intrinsic gain plus the two sources used to control gain. This is
+        // because we're changing the gain of a signal having constant value 1.
+        let success = true;
+        for (let i = 0; i < n; ++i) {
+          let expectedValue = baselineGain + linearRampData[i] + toneData[i];
+          let error = Math.abs(expectedValue - renderedData[i]);
+
+          if (error > maxAllowedError) {
+            success = false;
+            break;
+          }
+        }
+
+        should(
+            success,
+            'Rendered signal matches sum of two audio-rate gain changing signals plus baseline gain')
+            .beTrue();
+      }
+
+      audit.define('test', function(task, should) {
+        let sampleFrameLength = sampleRate * lengthInSeconds;
+
+        // Create offline audio context.
+        context = new OfflineAudioContext(1, sampleFrameLength, sampleRate);
+
+        // Create buffer used by the source which will have its gain controlled.
+        let constantOneBuffer =
+            createConstantBuffer(context, sampleFrameLength, 1);
+        let constantSource = context.createBufferSource();
+        constantSource.buffer = constantOneBuffer;
+
+        // Create 1st buffer used to control gain (a linear ramp).
+        linearRampBuffer = createLinearRampBuffer(context, sampleFrameLength);
+        let gainSource1 = context.createBufferSource();
+        gainSource1.buffer = linearRampBuffer;
+
+        // Create 2st buffer used to control gain (a simple sine wave tone).
+        toneBuffer =
+            createToneBuffer(context, toneFrequency, lengthInSeconds, 1);
+        let gainSource2 = context.createBufferSource();
+        gainSource2.buffer = toneBuffer;
+
+        // Create a gain node controlling the gain of constantSource and make
+        // the connections.
+        let gainNode = context.createGain();
+
+        // Intrinsic baseline gain.
+        // This gain value should be summed with gainSource1 and gainSource2.
+        gainNode.gain.value = baselineGain;
+
+        constantSource.connect(gainNode);
+        gainNode.connect(context.destination);
+
+        // Connect two audio-rate signals to control the .gain AudioParam.
+        gainSource1.connect(gainNode.gain);
+        gainSource2.connect(gainNode.gain);
+
+        // Start all sources at time 0.
+        constantSource.start(0);
+        gainSource1.start(0);
+        gainSource2.start(0);
+
+        context.startRendering().then(buffer => {
+          checkResult(buffer, should);
+          task.done();
+        });
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/automation-rate-testing.js b/webaudio/the-audio-api/the-audioparam-interface/automation-rate-testing.js
new file mode 100644
index 0000000..46d9d4a
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/automation-rate-testing.js
@@ -0,0 +1,117 @@
+// Test k-rate vs a-rate AudioParams.
+//
+// |options| describes how the testing of the AudioParam should be done:
+//
+//   nodeName:  name of the AudioNode to be tested
+//   nodeOptions:  options to be used in the AudioNode constructor
+//
+//   prefix: Prefix for all output messages (to make them unique for
+//           testharness)
+//
+//   rateSettings: A vector of dictionaries specifying how to set the automation
+//                 rate(s):
+//       name: Name of the AudioParam
+//       value: The automation rate for the AudioParam given by |name|.
+//
+//   automations: A vector of dictionaries specifying how to automate each
+//                AudioParam:
+//       name: Name of the AudioParam
+//
+//       methods: A vector of dictionaries specifying the automation methods to
+//                be used for testing:
+//           name: Automation method to call
+//           options: Arguments for the automation method
+//
+// Testing is somewhat rudimentary.  We create two nodes of the same type.  One
+// node uses the default automation rates for each AudioParam (expecting them to
+// be a-rate).  The second node sets the automation rate of AudioParams to
+// "k-rate".  The set is speciified by |options.rateSettings|.
+//
+// For both of these nodes, the same set of automation methods (given by
+// |options.automations|) is applied.  A simple oscillator is connected to each
+// node which in turn are connected to different channels of an offline context.
+// Channel 0 is the k-rate node output; channel 1, the a-rate output; and
+// channel 3, the difference between the outputs.
+//
+// Success is declared if the difference signal is not exactly zero.  This means
+// the the automations did different things, as expected.
+//
+// The promise from |startRendering| is returned.
+function doTest(context, should, options) {
+  let merger = new ChannelMergerNode(
+      context, {numberOfInputs: context.destination.numberOfChannels});
+  merger.connect(context.destination);
+
+  let src = new OscillatorNode(context);
+  let kRateNode = new window[options.nodeName](context, options.nodeOptions);
+  let aRateNode = new window[options.nodeName](context, options.nodeOptions);
+  let inverter = new GainNode(context, {gain: -1});
+
+  // Set kRateNode filter to use k-rate params.
+  options.rateSettings.forEach(setting => {
+    kRateNode[setting.name].automationRate = setting.value;
+    // Mostly for documentation in the output.  These should always
+    // pass.
+    should(
+        kRateNode[setting.name].automationRate,
+        `${options.prefix}: Setting ${
+                                      setting.name
+                                    }.automationRate to "${setting.value}"`)
+        .beEqualTo(setting.value);
+  });
+
+  // Run through all automations for each node separately. (Mostly to keep
+  // output of automations together.)
+  options.automations.forEach(param => {
+    param.methods.forEach(method => {
+      // Most for documentation in the output.  These should never throw.
+      let message = `${param.name}.${method.name}(${method.options})`
+      should(() => {
+        kRateNode[param.name][method.name](...method.options);
+      }, options.prefix + ': k-rate node: ' + message).notThrow();
+    });
+  });
+  options.automations.forEach(param => {
+    param.methods.forEach(method => {
+      // Most for documentation in the output.  These should never throw.
+      let message = `${param.name}.${method.name}(${method.options})`
+      should(() => {
+        aRateNode[param.name][method.name](...method.options);
+      }, options.prefix + ': a-rate node:' + message).notThrow();
+    });
+  });
+
+  // The k-rate result is channel 0, and the a-rate result is channel 1.
+  src.connect(kRateNode).connect(merger, 0, 0);
+  src.connect(aRateNode).connect(merger, 0, 1);
+
+  // Compute the difference between the a-rate and k-rate results and send
+  // that to channel 2.
+  kRateNode.connect(merger, 0, 2);
+  aRateNode.connect(inverter).connect(merger, 0, 2);
+
+  src.start();
+  return context.startRendering().then(renderedBuffer => {
+    let kRateOutput = renderedBuffer.getChannelData(0);
+    let aRateOutput = renderedBuffer.getChannelData(1);
+    let diff = renderedBuffer.getChannelData(2);
+
+    // Some informative messages to print out values of the k-rate and
+    // a-rate outputs.  These should always pass.
+    should(
+        kRateOutput, `${options.prefix}: Output of k-rate ${options.nodeName}`)
+        .beEqualToArray(kRateOutput);
+    should(
+        aRateOutput, `${options.prefix}: Output of a-rate ${options.nodeName}`)
+        .beEqualToArray(aRateOutput);
+
+    // The real test.  If k-rate AudioParam is working correctly, the
+    // k-rate result MUST differ from the a-rate result.
+    should(
+        diff,
+        `${
+           options.prefix
+         }: Difference between a-rate and k-rate ${options.nodeName}`)
+        .notBeConstantValueOf(0);
+  });
+}
diff --git a/webaudio/the-audio-api/the-audioparam-interface/automation-rate.html b/webaudio/the-audio-api/the-audioparam-interface/automation-rate.html
new file mode 100644
index 0000000..a3c789e
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/automation-rate.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<html>
+  <head>
+    <title>AudioParam.automationRate tests</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+
+  <body>
+    <script>
+      // For each node that has an AudioParam, verify that the default
+      // |automationRate| has the expected value and that we can change it or
+      // throw an error if it can't be changed.
+
+      // Any valid sample rate is fine; we don't actually render anything in the
+      // tests.
+      let sampleRate = 8000;
+
+      let audit = Audit.createTaskRunner();
+
+      // Array of tests.  Each test is a dictonary consisting of the name of the
+      // node and an array specifying the AudioParam's of the node.  This array
+      // in turn gives the name of the AudioParam, the default value for the
+      // |automationRate|, and whether it is fixed (isFixed).
+      const tests = [
+        {
+          nodeName: 'AudioBufferSourceNode',
+          audioParams: [
+            {name: 'detune', defaultRate: 'k-rate', isFixed: true},
+            {name: 'playbackRate', defaultRate: 'k-rate', isFixed: true}
+          ]
+        },
+        {
+          nodeName: 'BiquadFilterNode',
+          audioParams: [
+            {name: 'frequency', defaultRate: 'a-rate', isFixed: false},
+            {name: 'detune', defaultRate: 'a-rate', isFixed: false},
+            {name: 'Q', defaultRate: 'a-rate', isFixed: false},
+            {name: 'gain', defaultRate: 'a-rate', isFixed: false},
+          ]
+        },
+        {
+          nodeName: 'ConstantSourceNode',
+          audioParams: [{name: 'offset', defaultRate: 'a-rate', isFixed: false}]
+        },
+        {
+          nodeName: 'DelayNode',
+          audioParams:
+              [{name: 'delayTime', defaultRate: 'a-rate', isFixed: false}]
+        },
+        {
+          nodeName: 'DynamicsCompressorNode',
+          audioParams: [
+            {name: 'threshold', defaultRate: 'k-rate', isFixed: true},
+            {name: 'knee', defaultRate: 'k-rate', isFixed: true},
+            {name: 'ratio', defaultRate: 'k-rate', isFixed: true},
+            {name: 'attack', defaultRate: 'k-rate', isFixed: true},
+            {name: 'release', defaultRate: 'k-rate', isFixed: true}
+          ]
+        },
+        {
+          nodeName: 'GainNode',
+          audioParams: [{name: 'gain', defaultRate: 'a-rate', isFixed: false}]
+        },
+        {
+          nodeName: 'OscillatorNode',
+          audioParams: [
+            {name: 'frequency', defaultRate: 'a-rate', isFixed: false},
+            {name: 'detune', defaultRate: 'a-rate', isFixed: false}
+          ]
+        },
+        {
+          nodeName: 'PannerNode',
+          audioParams: [
+            {name: 'positionX', defaultRate: 'a-rate', isFixed: false},
+            {name: 'positionY', defaultRate: 'a-rate', isFixed: false},
+            {name: 'positionZ', defaultRate: 'a-rate', isFixed: false},
+            {name: 'orientationX', defaultRate: 'a-rate', isFixed: false},
+            {name: 'orientationY', defaultRate: 'a-rate', isFixed: false},
+            {name: 'orientationZ', defaultRate: 'a-rate', isFixed: false},
+          ]
+        },
+        {
+          nodeName: 'StereoPannerNode',
+          audioParams: [{name: 'pan', defaultRate: 'a-rate', isFixed: false}]
+        },
+      ];
+
+      tests.forEach(test => {
+        // Define a separate test for each test entry.
+        audit.define(test.nodeName, (task, should) => {
+          let context = new OfflineAudioContext(
+              {length: sampleRate, sampleRate: sampleRate});
+          // Construct the node and test each AudioParam of the node.
+          let node = new window[test.nodeName](context);
+          test.audioParams.forEach(param => {
+            testAudioParam(
+                should, {nodeName: test.nodeName, node: node, param: param});
+          });
+
+          task.done();
+        });
+      });
+
+      // AudioListener needs it's own special test since it's not a node.
+      audit.define('AudioListener', (task, should) => {
+        let context = new OfflineAudioContext(
+            {length: sampleRate, sampleRate: sampleRate});
+
+        [{name: 'positionX', defaultRate: 'a-rate', isFixed: false},
+         {name: 'positionY', defaultRate: 'a-rate', isFixed: false},
+         {name: 'positionZ', defaultRate: 'a-rate', isFixed: false},
+         {name: 'forwardX', defaultRate: 'a-rate', isFixed: false},
+         {name: 'forwardY', defaultRate: 'a-rate', isFixed: false},
+         {name: 'forwardZ', defaultRate: 'a-rate', isFixed: false},
+         {name: 'upX', defaultRate: 'a-rate', isFixed: false},
+         {name: 'upY', defaultRate: 'a-rate', isFixed: false},
+         {name: 'upZ', defaultRate: 'a-rate', isFixed: false},
+        ].forEach(param => {
+          testAudioParam(should, {
+            nodeName: 'AudioListener',
+            node: context.listener,
+            param: param
+          });
+        });
+        task.done();
+      });
+
+      audit.run();
+
+      function testAudioParam(should, options) {
+        let param = options.param;
+        let audioParam = options.node[param.name];
+        let defaultRate = param.defaultRate;
+
+        // Verify that the default value is correct.
+        should(
+            audioParam.automationRate,
+            `Default ${options.nodeName}.${param.name}.automationRate`)
+            .beEqualTo(defaultRate);
+
+        // Try setting the rate to a different rate.  If the |automationRate|
+        // is fixed, expect an error.  Otherwise, expect no error and expect
+        // the value is changed to the new value.
+        let newRate = defaultRate === 'a-rate' ? 'k-rate' : 'a-rate';
+        let setMessage = `Set ${
+                                options.nodeName
+                              }.${param.name}.automationRate to "${newRate}"`
+
+        if (param.isFixed) {
+          should(() => audioParam.automationRate = newRate, setMessage)
+              .throw('InvalidStateError');
+        }
+        else {
+          should(() => audioParam.automationRate = newRate, setMessage)
+              .notThrow();
+          should(
+              audioParam.automationRate,
+              `${options.nodeName}.${param.name}.automationRate`)
+              .beEqualTo(newRate);
+        }
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/event-insertion.html b/webaudio/the-audio-api/the-audioparam-interface/event-insertion.html
new file mode 100644
index 0000000..eab77c4
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/event-insertion.html
@@ -0,0 +1,314 @@
+<!doctype html>
+<html>
+  <head>
+    <title>
+      Test Handling of Event Insertion
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audio-param.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Use a power of two for the sample rate so there's no round-off in
+      // computing time from frame.
+      let sampleRate = 16384;
+
+      audit.define(
+          {label: 'Insert same event at same time'}, (task, should) => {
+            // Context for testing.
+            let context = new OfflineAudioContext(
+                {length: 16384, sampleRate: sampleRate});
+
+            // The source node to use.  Automations will be scheduled here.
+            let src = new ConstantSourceNode(context, {offset: 0});
+            src.connect(context.destination);
+
+            // An array of tests to be done.  Each entry specifies the event
+            // type and the event time.  The events are inserted in the order
+            // given (in |values|), and the second event should replace the
+            // first, as required by the spec.
+            let testCases = [
+              {
+                event: 'setValueAtTime',
+                frame: RENDER_QUANTUM_FRAMES,
+                values: [99, 1],
+                outputTestFrame: RENDER_QUANTUM_FRAMES,
+                expectedOutputValue: 1
+              },
+              {
+                event: 'linearRampToValueAtTime',
+                frame: 2 * RENDER_QUANTUM_FRAMES,
+                values: [99, 2],
+                outputTestFrame: 2 * RENDER_QUANTUM_FRAMES,
+                expectedOutputValue: 2
+              },
+              {
+                event: 'exponentialRampToValueAtTime',
+                frame: 3 * RENDER_QUANTUM_FRAMES,
+                values: [99, 3],
+                outputTestFrame: 3 * RENDER_QUANTUM_FRAMES,
+                expectedOutputValue: 3
+              },
+              {
+                event: 'setValueCurveAtTime',
+                frame: 3 * RENDER_QUANTUM_FRAMES,
+                values: [[98, 99], [3, 4]],
+                extraArgs: RENDER_QUANTUM_FRAMES / context.sampleRate,
+                outputTestFrame: 4 * RENDER_QUANTUM_FRAMES,
+                expectedOutputValue: 4
+              }
+            ];
+
+            testCases.forEach(entry => {
+              entry.values.forEach(value => {
+                let eventTime = entry.frame / context.sampleRate;
+                let message = eventToString(
+                    entry.event, value, eventTime, entry.extraArgs);
+                // This is mostly to print out the event that is getting
+                // inserted.  It should never ever throw.
+                should(() => {
+                  src.offset[entry.event](value, eventTime, entry.extraArgs);
+                }, message).notThrow();
+              });
+            });
+
+            src.start();
+
+            context.startRendering()
+                .then(audioBuffer => {
+                  let audio = audioBuffer.getChannelData(0);
+
+                  // Look through the test cases to figure out what the correct
+                  // output values should be.
+                  testCases.forEach(entry => {
+                    let expected = entry.expectedOutputValue;
+                    let frame = entry.outputTestFrame;
+                    let time = frame / context.sampleRate;
+                    should(
+                        audio[frame], `Output at frame ${frame} (time ${time})`)
+                        .beEqualTo(expected);
+                  });
+                })
+                .then(() => task.done());
+          });
+
+      audit.define(
+          {
+            label: 'Linear + Expo',
+            description: 'Different events at same time'
+          },
+          (task, should) => {
+            // Should be a linear ramp up to the event time, and after a
+            // constant value because the exponential ramp has ended.
+            let testCase = [
+              {event: 'linearRampToValueAtTime', value: 2, relError: 0},
+              {event: 'setValueAtTime', value: 99},
+              {event: 'exponentialRampToValueAtTime', value: 3},
+            ];
+            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
+            let prefix = 'Linear+Expo: ';
+
+            testEventInsertion(prefix, should, eventFrame, testCase)
+                .then(expectConstant(prefix, should, eventFrame, testCase))
+                .then(() => task.done());
+          });
+
+      audit.define(
+          {
+            label: 'Expo + Linear',
+            description: 'Different events at same time',
+          },
+          (task, should) => {
+            // Should be an exponential ramp up to the event time, and after a
+            // constant value because the linear ramp has ended.
+            let testCase = [
+              {
+                event: 'exponentialRampToValueAtTime',
+                value: 3,
+                relError: 4.2533e-6
+              },
+              {event: 'setValueAtTime', value: 99},
+              {event: 'linearRampToValueAtTime', value: 2},
+            ];
+            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
+            let prefix = 'Expo+Linear: ';
+
+            testEventInsertion(prefix, should, eventFrame, testCase)
+                .then(expectConstant(prefix, should, eventFrame, testCase))
+                .then(() => task.done());
+          });
+
+      audit.define(
+          {
+            label: 'Linear + SetTarget',
+            description: 'Different events at same time',
+          },
+          (task, should) => {
+            // Should be a linear ramp up to the event time, and then a
+            // decaying value.
+            let testCase = [
+              {event: 'linearRampToValueAtTime', value: 3, relError: 0},
+              {event: 'setValueAtTime', value: 100},
+              {event: 'setTargetAtTime', value: 0, extraArgs: 0.1},
+            ];
+            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
+            let prefix = 'Linear+SetTarget: ';
+
+            testEventInsertion(prefix, should, eventFrame, testCase)
+                .then(audioBuffer => {
+                  let audio = audioBuffer.getChannelData(0);
+                  let prefix = 'Linear+SetTarget: ';
+                  let eventTime = eventFrame / sampleRate;
+                  let expectedValue = methodMap[testCase[0].event](
+                      (eventFrame - 1) / sampleRate, 1, 0, testCase[0].value,
+                      eventTime);
+                  should(
+                      audio[eventFrame - 1],
+                      prefix +
+                          `At time ${
+                                     (eventFrame - 1) / sampleRate
+                                   } (frame ${eventFrame - 1}) output`)
+                      .beCloseTo(
+                          expectedValue,
+                          {threshold: testCase[0].relError || 0});
+
+                  // The setValue should have taken effect
+                  should(
+                      audio[eventFrame],
+                      prefix +
+                          `At time ${eventTime} (frame ${eventFrame}) output`)
+                      .beEqualTo(testCase[1].value);
+
+                  // The final event is setTarget.  Compute the expected output.
+                  let actual = audio.slice(eventFrame);
+                  let expected = new Float32Array(actual.length);
+                  for (let k = 0; k < expected.length; ++k) {
+                    let t = (eventFrame + k) / sampleRate;
+                    expected[k] = audioParamSetTarget(
+                        t, testCase[1].value, eventTime, testCase[2].value,
+                        testCase[2].extraArgs);
+                  }
+                  should(
+                      actual,
+                      prefix +
+                          `At time ${eventTime} (frame ${
+                                                         eventFrame
+                                                       }) and later`)
+                      .beCloseToArray(expected, {relativeThreshold: 1.7807e-7});
+                })
+                .then(() => task.done());
+          });
+
+
+      audit.run();
+
+      // Takes a list of |testCases| consisting of automation methods and
+      // schedules them to occur at |eventFrame|. |prefix| is a prefix for
+      // messages produced by |should|.
+      //
+      // Each item in |testCases| is a dictionary with members:
+      //   event     - the name of automation method to be inserted,
+      //   value     - the value for the event,
+      //   extraArgs - extra arguments if the event needs more than the value
+      //               and time (such as setTargetAtTime).
+      function testEventInsertion(prefix, should, eventFrame, testCases) {
+        let context = new OfflineAudioContext(
+            {length: 4 * RENDER_QUANTUM_FRAMES, sampleRate: sampleRate});
+
+        // The source node to use.  Automations will be scheduled here.
+        let src = new ConstantSourceNode(context, {offset: 0});
+        src.connect(context.destination);
+
+        // Initialize value to 1 at the beginning.
+        src.offset.setValueAtTime(1, 0);
+
+        // Test automations have this event time.
+        let eventTime = eventFrame / context.sampleRate;
+
+        // Sanity check that context is long enough for the test
+        should(
+            eventFrame < context.length,
+            prefix + 'Context length is long enough for the test')
+            .beTrue();
+
+        // Automations to be tested.  The first event should be the actual
+        // output up to the event time.  The last event should be the final
+        // output from the event time and onwards.
+        testCases.forEach(entry => {
+          should(
+              () => {
+                src.offset[entry.event](
+                    entry.value, eventTime, entry.extraArgs);
+              },
+              prefix +
+                  eventToString(
+                      entry.event, entry.value, eventTime, entry.extraArgs))
+              .notThrow();
+        });
+
+        src.start();
+
+        return context.startRendering();
+      }
+
+      // Verify output of test where the final value of the automation is
+      // expected to be constant.
+      function expectConstant(prefix, should, eventFrame, testCases) {
+        return audioBuffer => {
+          let audio = audioBuffer.getChannelData(0);
+
+          let eventTime = eventFrame / sampleRate;
+
+          // Compute the expected value of the first automation one frame before
+          // the event time.  This is a quick check that the correct automation
+          // was done.
+          let expectedValue = methodMap[testCases[0].event](
+              (eventFrame - 1) / sampleRate, 1, 0, testCases[0].value,
+              eventTime);
+          should(
+              audio[eventFrame - 1],
+              prefix +
+                  `At time ${
+                             (eventFrame - 1) / sampleRate
+                           } (frame ${eventFrame - 1}) output`)
+              .beCloseTo(expectedValue, {threshold: testCases[0].relError});
+
+          // The last event scheduled is expected to set the value for all
+          // future times.  Verify that the output has the expected value.
+          should(
+              audio.slice(eventFrame),
+              prefix +
+                  `At time ${eventTime} (frame ${
+                                                 eventFrame
+                                               }) and later, output`)
+              .beConstantValueOf(testCases[testCases.length - 1].value);
+        };
+      }
+
+      // Convert an automation method to a string for printing.
+      function eventToString(method, value, time, extras) {
+        let string = method + '(';
+        string += (value instanceof Array) ? `[${value}]` : value;
+        string += ', ' + time;
+        if (extras) {
+          string += ', ' + extras;
+        }
+        string += ')';
+        return string;
+      }
+
+      // Map between the automation method name and a function that computes the
+      // output value of the automation method.
+      const methodMap = {
+        linearRampToValueAtTime: audioParamLinearRamp,
+        exponentialRampToValueAtTime: audioParamExponentialRamp,
+        setValueAtTime: (t, v) => v
+      };
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-audioworklet.https.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-audioworklet.https.html
new file mode 100644
index 0000000..e891da6
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-audioworklet.https.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParam of AudioWorkletNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+
+  <body>
+    <script>
+      const audit = Audit.createTaskRunner();
+
+      // Use the worklet gain node to test k-rate parameters.
+      const filePath =
+          '../the-audioworklet-interface/processors/gain-processor.js';
+
+      // Context for testing
+      let context;
+
+      audit.define('Create Test Worklet', (task, should) => {
+
+        // Arbitrary sample rate and duration.
+        const sampleRate = 8000;
+
+        // Only new a few render quanta to verify things are working.
+        const testDuration = 4 * 128 / sampleRate;
+
+        context = new OfflineAudioContext({
+          numberOfChannels: 3,
+          sampleRate: sampleRate,
+          length: testDuration * sampleRate
+        });
+
+        should(
+            context.audioWorklet.addModule(filePath),
+            'Construction of AudioWorklet')
+            .beResolved()
+            .then(() => task.done());
+      });
+
+      audit.define('AudioWorklet k-rate AudioParam', (task, should) => {
+        let src = new ConstantSourceNode(context);
+
+        let kRateNode = new AudioWorkletNode(context, 'gain');
+
+        src.connect(kRateNode).connect(context.destination);
+
+        let kRateParam = kRateNode.parameters.get('gain');
+        kRateParam.automationRate = 'k-rate';
+
+        // Automate the gain
+        kRateParam.setValueAtTime(0, 0);
+        kRateParam.linearRampToValueAtTime(
+            10, context.length / context.sampleRate);
+
+        src.start();
+
+        context.startRendering()
+            .then(audioBuffer => {
+              let output = audioBuffer.getChannelData(0);
+
+              // Verify that the output from the worklet is step-wise
+              // constant.
+              for (let k = 0; k < output.length; k += 128) {
+                should(
+                    output.slice(k, k + 128),
+                    ` k-rate output [${k}: ${k + 127}]`)
+                    .beConstantValueOf(output[k]);
+              }
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-biquad.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-biquad.html
new file mode 100644
index 0000000..85ae4f1
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-biquad.html
@@ -0,0 +1,111 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParams of BiquadFilterNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="automation-rate-testing.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {task: 'BiquadFilter-0', label: 'Biquad k-rate AudioParams (all)'},
+          (task, should) => {
+            // Arbitrary sample rate and duration.
+            let sampleRate = 8000;
+            let testDuration = 1;
+            let context = new OfflineAudioContext({
+              numberOfChannels: 3,
+              sampleRate: sampleRate,
+              length: testDuration * sampleRate
+            });
+
+            doTest(context, should, {
+              nodeName: 'BiquadFilterNode',
+              nodeOptions: {type: 'lowpass'},
+              prefix: 'All k-rate params',
+              // Set all AudioParams to k-rate
+              rateSettings: [
+                {name: 'Q', value: 'k-rate'},
+                {name: 'detune', value: 'k-rate'},
+                {name: 'frequency', value: 'k-rate'},
+                {name: 'gain', value: 'k-rate'},
+              ],
+              // Automate just the frequency
+              automations: [{
+                name: 'frequency',
+                methods: [
+                  {name: 'setValueAtTime', options: [350, 0]}, {
+                    name: 'linearRampToValueAtTime',
+                    options: [0, testDuration]
+                  }
+                ]
+              }]
+            }).then(() => task.done());
+          });
+
+      // Define a test where we verify that a k-rate audio param produces
+      // different results from an a-rate audio param for each of the audio
+      // params of a biquad.
+      //
+      // Each entry gives the name of the AudioParam, an initial value to be
+      // used with setValueAtTime, and a final value to be used with
+      // linearRampToValueAtTime. (See |doTest| for details as well.)
+
+      [{name: 'Q',
+        initial: 1,
+        final: 10
+       },
+       {name: 'detune',
+        initial: 0,
+        final: 1200
+       },
+       {name: 'frequency',
+        initial: 350,
+        final: 0
+       },
+       {name: 'gain',
+        initial: 10,
+        final: 0
+       }].forEach(paramProperty => {
+        audit.define('Biquad k-rate ' + paramProperty.name, (task, should) => {
+          // Arbitrary sample rate and duration.
+          let sampleRate = 8000;
+          let testDuration = 1;
+          let context = new OfflineAudioContext({
+            numberOfChannels: 3,
+            sampleRate: sampleRate,
+            length: testDuration * sampleRate
+          });
+
+          doTest(context, should, {
+            nodeName: 'BiquadFilterNode',
+            nodeOptions: {type: 'peaking', Q: 1, gain: 10},
+            prefix: `k-rate ${paramProperty.name}`,
+            // Just set the frequency to k-rate
+            rateSettings: [
+              {name: paramProperty.name, value: 'k-rate'},
+            ],
+            // Automate just the given AudioParam
+            automations: [{
+              name: paramProperty.name,
+              methods: [
+                {name: 'setValueAtTime', options: [paramProperty.initial, 0]}, {
+                  name: 'linearRampToValueAtTime',
+                  options: [paramProperty.final, testDuration]
+                }
+              ]
+            }]
+          }).then(() => task.done());
+        });
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-constant-source.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-constant-source.html
new file mode 100644
index 0000000..5c421d0
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-constant-source.html
@@ -0,0 +1,81 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParam of ConstantSourceNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      audit.define('ConstantSource k-rate offset', (task, should) => {
+        // Arbitrary sample rate and duration.
+        let sampleRate = 8000;
+
+        // Only new a few render quanta to verify things are working.
+        let testDuration = 4 * 128 / sampleRate;
+
+        let context = new OfflineAudioContext({
+          numberOfChannels: 3,
+          sampleRate: sampleRate,
+          length: testDuration * sampleRate
+        });
+
+        let merger = new ChannelMergerNode(
+            context, {numberOfInputs: context.numberOfChannels});
+        merger.connect(context.destination);
+        let inverter = new GainNode(context, {gain: -1});
+        inverter.connect(merger, 0, 2);
+
+        let kRateNode = new ConstantSourceNode(context);
+        let aRateNode = new ConstantSourceNode(context);
+
+        kRateNode.connect(merger, 0, 0);
+        aRateNode.connect(merger, 0, 1);
+
+        kRateNode.connect(merger, 0, 2);
+        aRateNode.connect(inverter);
+
+        // Set the rate
+        kRateNode.offset.automationRate = 'k-rate';
+
+        // Automate the offset
+        kRateNode.offset.setValueAtTime(0, 0);
+        kRateNode.offset.linearRampToValueAtTime(10, testDuration);
+
+        aRateNode.offset.setValueAtTime(0, 0);
+        aRateNode.offset.linearRampToValueAtTime(10, testDuration);
+
+        kRateNode.start();
+        aRateNode.start();
+
+        context.startRendering()
+            .then(audioBuffer => {
+              let kRateOut = audioBuffer.getChannelData(0);
+              let aRateOut = audioBuffer.getChannelData(1);
+              let diff = audioBuffer.getChannelData(2);
+
+              // Verify that the outputs are different.
+              should(diff, 'Difference between a-rate and k-rate outputs')
+                  .notBeConstantValueOf(0);
+
+              // Verify that the constant source node output is step-wise
+              // constant.
+              for (let k = 0; k < kRateOut.length; k += 128) {
+                should(
+                    kRateOut.slice(k, k + 128),
+                    `k-rate output [${k}: ${k + 127}]`)
+                    .beConstantValueOf(kRateOut[k]);
+              }
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-delay.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-delay.html
new file mode 100644
index 0000000..5465c39
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-delay.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParam of DelayNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="automation-rate-testing.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      audit.define('Test k-rate DelayNode', (task, should) => {
+        // Arbitrary sample rate and duration.
+        let sampleRate = 8000;
+        let testDuration = 1;
+        let context = new OfflineAudioContext({
+          numberOfChannels: 3,
+          sampleRate: sampleRate,
+          length: testDuration * sampleRate
+        });
+
+
+        doTest(context, should, {
+          nodeName: 'DelayNode',
+          nodeOptions: null,
+          prefix: 'DelayNode',
+          // Set all AudioParams to k-rate
+          rateSettings: [{name: 'delayTime', value: 'k-rate'}],
+          // Automate just the frequency
+          automations: [{
+            name: 'delayTime',
+            methods: [
+              {name: 'setValueAtTime', options: [0, 0]}, {
+                name: 'linearRampToValueAtTime',
+                options: [.5, testDuration]
+              }
+            ]
+          }]
+        }).then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-gain.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-gain.html
new file mode 100644
index 0000000..887d9f7
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-gain.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParam of GainNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="automation-rate-testing.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      audit.define('Test k-rate GainNode', (task, should) => {
+        // Arbitrary sample rate and duration.
+        let sampleRate = 8000;
+        let testDuration = 1;
+        let context = new OfflineAudioContext({
+          numberOfChannels: 3,
+          sampleRate: sampleRate,
+          length: testDuration * sampleRate
+        });
+
+
+        doTest(context, should, {
+          nodeName: 'GainNode',
+          nodeOptions: null,
+          prefix: 'GainNode',
+          // Set AudioParam to k-rate
+          rateSettings: [{name: 'gain', value: 'k-rate'}],
+          // Automate
+          automations: [{
+            name: 'gain',
+            methods: [
+              {name: 'setValueAtTime', options: [1, 0]},
+              {name: 'linearRampToValueAtTime', options: [0, testDuration]}
+            ]
+          }]
+        }).then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-oscillator.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-oscillator.html
new file mode 100644
index 0000000..1672f0d
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-oscillator.html
@@ -0,0 +1,88 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParams of OscillatorNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      // Arbitrary sample rate and duration.
+      let sampleRate = 8000;
+
+      // Only new a few render quanta to verify things are working.
+      let testDuration = 4 * 128 / sampleRate;
+
+      [{name: 'detune', initial: 0, final: 1200}, {
+        name: 'frequency',
+        initial: 440,
+        final: sampleRate / 2
+      }].forEach(paramProperty => {
+        audit.define(
+            'Oscillator k-rate ' + paramProperty.name, (task, should) => {
+              let context = new OfflineAudioContext({
+                numberOfChannels: 3,
+                sampleRate: sampleRate,
+                length: testDuration * sampleRate
+              });
+
+              let merger = new ChannelMergerNode(
+                  context, {numberOfInputs: context.numberOfChannels});
+              merger.connect(context.destination);
+              let inverter = new GainNode(context, {gain: -1});
+              inverter.connect(merger, 0, 2);
+
+              let kRateNode = new OscillatorNode(context);
+              let aRateNode = new OscillatorNode(context);
+
+              kRateNode.connect(merger, 0, 0);
+              aRateNode.connect(merger, 0, 1);
+
+              kRateNode.connect(merger, 0, 2);
+              aRateNode.connect(inverter);
+
+              // Set the rate
+              kRateNode[paramProperty.name].automationRate = 'k-rate';
+
+              // Automate the offset
+              kRateNode[paramProperty.name].setValueAtTime(
+                  paramProperty.initial, 0);
+              kRateNode[paramProperty.name].linearRampToValueAtTime(
+                  paramProperty.final, testDuration);
+
+              aRateNode[paramProperty.name].setValueAtTime(
+                  paramProperty.initial, 0);
+              aRateNode[paramProperty.name].linearRampToValueAtTime(
+                  paramProperty.final, testDuration);
+
+              kRateNode.start();
+              aRateNode.start();
+
+              context.startRendering()
+                  .then(audioBuffer => {
+                    let kRateOut = audioBuffer.getChannelData(0);
+                    let aRateOut = audioBuffer.getChannelData(1);
+                    let diff = audioBuffer.getChannelData(2);
+
+                    // Verify that the outputs are different.
+                    should(
+                        diff,
+                        'k-rate ' + paramProperty.name +
+                            ': Difference between a-rate and k-rate outputs')
+                        .notBeConstantValueOf(0);
+
+                  })
+                  .then(() => task.done());
+            });
+      });
+
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-panner.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-panner.html
new file mode 100644
index 0000000..1e9e900
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-panner.html
@@ -0,0 +1,172 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParams of PannerNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="automation-rate-testing.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      // Define a test where we verify that a k-rate audio param produces
+      // different results from an a-rate audio param for each of the audio
+      // params of a biquad.
+      //
+      // Each entry gives the name of the AudioParam, an initial value to be
+      // used with setValueAtTime, and a final value to be used with
+      // linearRampToValueAtTime. (See |doTest| for details as well.)
+
+      [{name: 'positionX', initial: 0, final: 1000},
+       {name: 'positionY', initial: 0, final: 1000},
+       {name: 'orientationX', initial: .1, final: 1000},
+       {name: 'orientationY', initial: .1, final: 1000},
+       {name: 'orientationZ', initial: .1, final: 1000},
+      ].forEach(paramProperty => {
+        audit.define('Panner k-rate ' + paramProperty.name, (task, should) => {
+          // Arbitrary sample rate and duration.
+          let sampleRate = 8000;
+          let testDuration = 1;
+          let context = new OfflineAudioContext({
+            numberOfChannels: 3,
+            sampleRate: sampleRate,
+            length: testDuration * sampleRate
+          });
+
+          doTest(context, should, {
+            nodeName: 'PannerNode',
+            // Make the source directional so orientation matters, and set some
+            // defaults for the position and orientation so that we're not on an
+            // axis where the azimuth and elevation might be constant when
+            // moving one of the AudioParams.
+            nodeOptions: {
+              distanceModel: 'inverse',
+              coneOuterAngle: 360,
+              coneInnerAngle: 10,
+              positionX: 10,
+              positionY: 10,
+              positionZ: 10,
+              orientationX: 1,
+              orientationY: 1,
+              orientationZ: 1
+            },
+            prefix: `k-rate ${paramProperty.name}`,
+            // Just set the frequency to k-rate
+            rateSettings: [
+              {name: paramProperty.name, value: 'k-rate'},
+            ],
+            // Automate just the given AudioParam
+            automations: [{
+              name: paramProperty.name,
+              methods: [
+                {name: 'setValueAtTime', options: [paramProperty.initial, 0]}, {
+                  name: 'linearRampToValueAtTime',
+                  options: [paramProperty.final, testDuration]
+                }
+              ]
+            }]
+          }).then(() => task.done());
+        });
+      });
+
+      // Test k-rate automation of the listener.  The intial and final
+      // automation values are pretty arbitrary, except that they should be such
+      // that the panner and listener produces non-constant output.
+      [{name: 'positionX', initial: [1, 0], final: [1000, 1]},
+       {name: 'positionY', initial: [1, 0], final: [1000, 1]},
+       {name: 'positionZ', initial: [1, 0], final: [1000, 1]},
+       {name: 'forwardX', initial: [-1, 0], final: [1, 1]},
+       {name: 'forwardY', initial: [-1, 0], final: [1, 1]},
+       {name: 'forwardZ', initial: [-1, 0], final: [1, 1]},
+       {name: 'upX', initial: [-1, 0], final: [1000, 1]},
+       {name: 'upY', initial: [-1, 0], final: [1000, 1]},
+       {name: 'upZ', initial: [-1, 0], final: [1000, 1]},
+      ].forEach(paramProperty => {
+        audit.define(
+            'Listener k-rate ' + paramProperty.name, (task, should) => {
+              // Arbitrary sample rate and duration.
+              let sampleRate = 8000;
+              let testDuration = 5 * 128 / sampleRate;
+              let context = new OfflineAudioContext({
+                numberOfChannels: 1,
+                sampleRate: sampleRate,
+                length: testDuration * sampleRate
+              });
+
+              doListenerTest(context, should, {
+                param: paramProperty.name,
+                initial: paramProperty.initial,
+                final: paramProperty.final
+              }).then(() => task.done());
+            });
+      });
+
+      audit.run();
+
+      function doListenerTest(context, should, options) {
+        let src = new ConstantSourceNode(context);
+        let panner = new PannerNode(context, {
+          distanceModel: 'inverse',
+          coneOuterAngle: 360,
+          coneInnerAngle: 10,
+          positionX: 10,
+          positionY: 10,
+          positionZ: 10,
+          orientationX: 1,
+          orientationY: 1,
+          orientationZ: 1
+        });
+
+        src.connect(panner).connect(context.destination);
+
+        src.start();
+
+        let listener = context.listener;
+
+        // Set listener properties to "random" values so that motion on one of
+        // the attributes actually changes things relative to the panner
+        // location.
+        listener.positionX.value = -1;
+        listener.positionY.value = 1;
+        listener.positionZ.value = -1;
+        listener.forwardX.value = -1;
+        listener.forwardY.value = 1;
+        listener.forwardZ.value = -1;
+        listener.upX.value = 1;
+        listener.upY.value = 1;
+        listener.upZ.value = 1;
+
+        let audioParam = listener[options.param];
+        audioParam.automationRate = 'k-rate';
+
+        let prefix = `Listener ${options.param}`;
+        should(audioParam.automationRate, prefix + '.automationRate')
+            .beEqualTo('k-rate');
+        should(() => {
+          audioParam.setValueAtTime(...options.initial);
+        }, prefix + `.setValueAtTime(${options.initial})`).notThrow();
+        should(() => {
+          audioParam.linearRampToValueAtTime(...options.final);
+        }, prefix + `.linearRampToValueAtTime(${options.final})`).notThrow();
+
+        return context.startRendering().then(renderedBuffer => {
+          let prefix = `Listener k-rate ${options.param}: `;
+          let output = renderedBuffer.getChannelData(0);
+          // Sanity check that the output isn't constant.
+          should(output, prefix + `Output`).notBeConstantValueOf(output[0]);
+
+          // Verify that the output is constant over each render quantum
+          for (let k = 0; k < output.length; k += 128) {
+            should(
+                output.slice(k, k + 128), prefix + `Output [${k}, ${k + 127}]`)
+                .beConstantValueOf(output[k]);
+          }
+        });
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioparam-interface/k-rate-stereo-panner.html b/webaudio/the-audio-api/the-audioparam-interface/k-rate-stereo-panner.html
new file mode 100644
index 0000000..06905b8
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioparam-interface/k-rate-stereo-panner.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test k-rate AudioParam of StereoPannerNode</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="automation-rate-testing.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      audit.define('Test k-rate StereoPannerNode', (task, should) => {
+        // Arbitrary sample rate and duration.
+        let sampleRate = 8000;
+        let testDuration = 1;
+        let context = new OfflineAudioContext({
+          numberOfChannels: 3,
+          sampleRate: sampleRate,
+          length: testDuration * sampleRate
+        });
+
+        doTest(context, should, {
+          nodeName: 'StereoPannerNode',
+          nodeOptions: null,
+          prefix: 'StereoPannerNode',
+          // Set all AudioParams to k-rate.
+          rateSettings: [{name: 'pan', value: 'k-rate'}],
+          // Automate just the frequency.
+          automations: [{
+            name: 'pan',
+            methods: [
+              {name: 'setValueAtTime', options: [0, 0]}, {
+                name: 'linearRampToValueAtTime',
+                options: [.5, testDuration]
+              }
+            ]
+          }]
+        }).then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-audioworklet-interface/audioworkletnode-automatic-pull.https.html b/webaudio/the-audio-api/the-audioworklet-interface/audioworkletnode-automatic-pull.https.html
new file mode 100644
index 0000000..330b359
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioworklet-interface/audioworkletnode-automatic-pull.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test AudioWorkletNode's automatic pull feature
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      const audit = Audit.createTaskRunner();
+
+      // Arbitrary sample rate. Anything should work.
+      const sampleRate = 48000;
+      const renderLength = RENDER_QUANTUM_FRAMES * 2;
+      const channelCount = 1;
+      const filePath = 'processors/zero-output-processor.js';
+
+      const sourceOffset = 0.5;
+
+      // Connect a constant source node to the zero-output AudioWorkletNode.
+      // Then verify if it captures the data correctly.
+      audit.define('setup-worklet', (task, should) => {
+        const context =
+            new OfflineAudioContext(channelCount, renderLength, sampleRate);
+
+        context.audioWorklet.addModule(filePath).then(() => {
+          let testSource =
+              new ConstantSourceNode(context, { offset: sourceOffset });
+          let zeroOutputWorkletNode =
+              new AudioWorkletNode(context, 'zero-output-processor', {
+                numberOfInputs: 1,
+                numberOfOutputs: 0,
+                processorOptions: {
+                  bufferLength: renderLength,
+                  channeCount: channelCount
+                }
+              });
+
+          // Start the source and stop at the first render quantum.
+          testSource.connect(zeroOutputWorkletNode);
+          testSource.start();
+          testSource.stop(RENDER_QUANTUM_FRAMES/sampleRate);
+
+          zeroOutputWorkletNode.port.onmessage = (event) => {
+            // The |capturedBuffer| can be multichannel. Iterate through it.
+            for (let i = 0; i < event.data.capturedBuffer.length; ++i) {
+              let buffer = event.data.capturedBuffer[i];
+              // Split the captured buffer in half for the easier test.
+              should(buffer.subarray(0, RENDER_QUANTUM_FRAMES),
+                     'The first half of the captured buffer')
+                  .beConstantValueOf(sourceOffset);
+              should(buffer.subarray(RENDER_QUANTUM_FRAMES, renderLength),
+                     'The second half of the captured buffer')
+                  .beConstantValueOf(0);
+            }
+            task.done();
+          };
+
+          // Starts the rendering, but we don't need the rendered buffer from
+          // the context.
+          context.startRendering();
+        });
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
+
diff --git a/webaudio/the-audio-api/the-audioworklet-interface/processors/zero-output-processor.js b/webaudio/the-audio-api/the-audioworklet-interface/processors/zero-output-processor.js
new file mode 100644
index 0000000..b97ed6e
--- /dev/null
+++ b/webaudio/the-audio-api/the-audioworklet-interface/processors/zero-output-processor.js
@@ -0,0 +1,42 @@
+/**
+ * @class ZeroOutputProcessor
+ * @extends AudioWorkletProcessor
+ *
+ * This processor accumulates the incoming buffer and send the buffered data
+ * to the main thread when it reaches the specified frame length. The processor
+ * only supports the single input.
+ */
+
+const kRenderQuantumFrames = 128;
+
+class ZeroOuttputProcessor extends AudioWorkletProcessor {
+  constructor(options) {
+    super();
+
+    this._framesRequested = options.processorOptions.bufferLength;
+    this._framesCaptured = 0;
+    this._buffer = [];
+    for (let i = 0; i < options.processorOptions.channeCount; ++i) {
+      this._buffer[i] = new Float32Array(this._framesRequested);
+    }
+  }
+
+  process(inputs) {
+    let input = inputs[0];
+    let startIndex = this._framesCaptured;
+    let endIndex = startIndex + kRenderQuantumFrames;
+    for (let i = 0; i < this._buffer.length; ++i) {
+      this._buffer[i].subarray(startIndex, endIndex).set(input[i]);
+    }
+    this._framesCaptured = endIndex;
+
+    if (this._framesCaptured >= this._framesRequested) {
+      this.port.postMessage({ capturedBuffer: this._buffer });
+      return false;
+    } else {
+      return true;
+    }
+  }
+}
+
+registerProcessor('zero-output-processor', ZeroOuttputProcessor);
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html
new file mode 100644
index 0000000..86618f9
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-allpass.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad allpass filter'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            let filterParameters = [
+              {cutoff: 0, q: 10, gain: 1},
+              {cutoff: 1, q: 10, gain: 1},
+              {cutoff: .5, q: 0, gain: 1},
+              {cutoff: 0.25, q: 10, gain: 1},
+            ];
+            createTestAndRun(context, 'allpass', {
+              should: should,
+              threshold: 3.9337e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html
new file mode 100644
index 0000000..54b2142
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html
@@ -0,0 +1,406 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Biquad Automation Test
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/audioparam-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      // Don't need to run these tests at high sampling rate, so just use a low
+      // one to reduce memory usage and complexity.
+      let sampleRate = 16000;
+
+      // How long to render for each test.
+      let renderDuration = 0.25;
+      // Where to end the automations.  Fairly arbitrary, but must end before
+      // the renderDuration.
+      let automationEndTime = renderDuration / 2;
+
+      let audit = Audit.createTaskRunner();
+
+      // The definition of the linear ramp automation function.
+      function linearRamp(t, v0, v1, t0, t1) {
+        return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
+      }
+
+      // Generate the filter coefficients for the specified filter using the
+      // given parameters for the given duration.  |filterTypeFunction| is a
+      // function that returns the filter coefficients for one set of
+      // parameters.  |parameters| is a property bag that contains the start and
+      // end values (as an array) for each of the biquad attributes.  The
+      // properties are |freq|, |Q|, |gain|, and |detune|.  |duration| is the
+      // number of seconds for which the coefficients are generated.
+      //
+      // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|.  Each
+      // propery is an array consisting of the coefficients for the time-varying
+      // biquad filter.
+      function generateFilterCoefficients(
+          filterTypeFunction, parameters, duration) {
+        let renderEndFrame = Math.ceil(renderDuration * sampleRate);
+        let endFrame = Math.ceil(duration * sampleRate);
+        let nCoef = renderEndFrame;
+        let b0 = new Float64Array(nCoef);
+        let b1 = new Float64Array(nCoef);
+        let b2 = new Float64Array(nCoef);
+        let a1 = new Float64Array(nCoef);
+        let a2 = new Float64Array(nCoef);
+
+        let k = 0;
+        // If the property is not given, use the defaults.
+        let freqs = parameters.freq || [350, 350];
+        let qs = parameters.Q || [1, 1];
+        let gains = parameters.gain || [0, 0];
+        let detunes = parameters.detune || [0, 0];
+
+        for (let frame = 0; frame <= endFrame; ++frame) {
+          // Apply linear ramp at frame |frame|.
+          let f =
+              linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration);
+          let q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration);
+          let g =
+              linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration);
+          let d = linearRamp(
+              frame / sampleRate, detunes[0], detunes[1], 0, duration);
+
+          // Compute actual frequency parameter
+          f = f * Math.pow(2, d / 1200);
+
+          // Compute filter coefficients
+          let coef = filterTypeFunction(f / (sampleRate / 2), q, g);
+          b0[k] = coef.b0;
+          b1[k] = coef.b1;
+          b2[k] = coef.b2;
+          a1[k] = coef.a1;
+          a2[k] = coef.a2;
+          ++k;
+        }
+
+        // Fill the rest of the arrays with the constant value to the end of
+        // the rendering duration.
+        b0.fill(b0[endFrame], endFrame + 1);
+        b1.fill(b1[endFrame], endFrame + 1);
+        b2.fill(b2[endFrame], endFrame + 1);
+        a1.fill(a1[endFrame], endFrame + 1);
+        a2.fill(a2[endFrame], endFrame + 1);
+
+        return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2};
+      }
+
+      // Apply the given time-varying biquad filter to the given signal,
+      // |signal|.  |coef| should be the time-varying coefficients of the
+      // filter, as returned by |generateFilterCoefficients|.
+      function timeVaryingFilter(signal, coef) {
+        let length = signal.length;
+        // Use double precision for the internal computations.
+        let y = new Float64Array(length);
+
+        // Prime the pump. (Assumes the signal has length >= 2!)
+        y[0] = coef.b0[0] * signal[0];
+        y[1] =
+            coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0];
+
+        for (let n = 2; n < length; ++n) {
+          y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n - 1] +
+              coef.b2[n] * signal[n - 2];
+          y[n] -= coef.a1[n] * y[n - 1] + coef.a2[n] * y[n - 2];
+        }
+
+        // But convert the result to single precision for comparison.
+        return y.map(Math.fround);
+      }
+
+      // Configure the audio graph using |context|.  Returns the biquad filter
+      // node and the AudioBuffer used for the source.
+      function configureGraph(context, toneFrequency) {
+        // The source is just a simple sine wave.
+        let src = context.createBufferSource();
+        let b =
+            context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+        let data = b.getChannelData(0);
+        let omega = 2 * Math.PI * toneFrequency / sampleRate;
+        for (let k = 0; k < data.length; ++k) {
+          data[k] = Math.sin(omega * k);
+        }
+        src.buffer = b;
+        let f = context.createBiquadFilter();
+        src.connect(f);
+        f.connect(context.destination);
+
+        src.start();
+
+        return {filter: f, source: b};
+      }
+
+      function createFilterVerifier(
+          should, filterCreator, threshold, parameters, input, message) {
+        return function(resultBuffer) {
+          let actual = resultBuffer.getChannelData(0);
+          let coefs = generateFilterCoefficients(
+              filterCreator, parameters, automationEndTime);
+
+          reference = timeVaryingFilter(input, coefs);
+
+          should(actual, message).beCloseToArray(reference, {
+            absoluteThreshold: threshold
+          });
+        };
+      }
+
+      // Automate just the frequency parameter.  A bandpass filter is used where
+      // the center frequency is swept across the source (which is a simple
+      // tone).
+      audit.define('automate-freq', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+        // Center frequency of bandpass filter and also the frequency of the
+        // test tone.
+        let centerFreq = 10 * 440;
+
+        // Sweep the frequency +/- 5*440 Hz from the center.  This should cause
+        // the output to be low at the beginning and end of the test where the
+        // tone is outside the pass band of the filter, but high in the middle
+        // of the automation time where the tone is near the center of the pass
+        // band.  Make sure the frequency sweep stays inside the Nyquist
+        // frequency.
+        let parameters = {freq: [centerFreq - 5 * 440, centerFreq + 5 * 440]};
+        let graph = configureGraph(context, centerFreq);
+        let f = graph.filter;
+        let b = graph.source;
+
+        f.type = 'bandpass';
+        f.frequency.setValueAtTime(parameters.freq[0], 0);
+        f.frequency.linearRampToValueAtTime(
+            parameters.freq[1], automationEndTime);
+
+        context.startRendering()
+            .then(createFilterVerifier(
+                should, createBandpassFilter, 4.6455e-6, parameters,
+                b.getChannelData(0),
+                'Output of bandpass filter with frequency automation'))
+            .then(() => task.done());
+      });
+
+      // Automate just the Q parameter.  A bandpass filter is used where the Q
+      // of the filter is swept.
+      audit.define('automate-q', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+        // The frequency of the test tone.
+        let centerFreq = 440;
+
+        // Sweep the Q paramter between 1 and 200.  This will cause the output
+        // of the filter to pass most of the tone at the beginning to passing
+        // less of the tone at the end.  This is because we set center frequency
+        // of the bandpass filter to be slightly off from the actual tone.
+        let parameters = {
+          Q: [1, 200],
+          // Center frequency of the bandpass filter is just 25 Hz above the
+          // tone frequency.
+          freq: [centerFreq + 25, centerFreq + 25]
+        };
+        let graph = configureGraph(context, centerFreq);
+        let f = graph.filter;
+        let b = graph.source;
+
+        f.type = 'bandpass';
+        f.frequency.value = parameters.freq[0];
+        f.Q.setValueAtTime(parameters.Q[0], 0);
+        f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);
+
+        context.startRendering()
+            .then(createFilterVerifier(
+                should, createBandpassFilter, 9.8348e-7, parameters,
+                b.getChannelData(0),
+                'Output of bandpass filter with Q automation'))
+            .then(() => task.done());
+      });
+
+      // Automate just the gain of the lowshelf filter.  A test tone will be in
+      // the lowshelf part of the filter.  The output will vary as the gain of
+      // the lowshelf is changed.
+      audit.define('automate-gain', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+        // Frequency of the test tone.
+        let centerFreq = 440;
+
+        // Set the cutoff frequency of the lowshelf to be significantly higher
+        // than the test tone. Sweep the gain from 20 dB to -20 dB.  (We go from
+        // 20 to -20 to easily verify that the filter didn't go unstable.)
+        let parameters = {freq: [3500, 3500], gain: [20, -20]};
+        let graph = configureGraph(context, centerFreq);
+        let f = graph.filter;
+        let b = graph.source;
+
+        f.type = 'lowshelf';
+        f.frequency.value = parameters.freq[0];
+        f.gain.setValueAtTime(parameters.gain[0], 0);
+        f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);
+
+        context.startRendering()
+            .then(createFilterVerifier(
+                should, createLowShelfFilter, 2.7657e-5, parameters,
+                b.getChannelData(0),
+                'Output of lowshelf filter with gain automation'))
+            .then(() => task.done());
+      });
+
+      // Automate just the detune parameter.  Basically the same test as for the
+      // frequncy parameter but we just use the detune parameter to modulate the
+      // frequency parameter.
+      audit.define('automate-detune', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+        let centerFreq = 10 * 440;
+        let parameters = {
+          freq: [centerFreq, centerFreq],
+          detune: [-10 * 1200, 10 * 1200]
+        };
+        let graph = configureGraph(context, centerFreq);
+        let f = graph.filter;
+        let b = graph.source;
+
+        f.type = 'bandpass';
+        f.frequency.value = parameters.freq[0];
+        f.detune.setValueAtTime(parameters.detune[0], 0);
+        f.detune.linearRampToValueAtTime(
+            parameters.detune[1], automationEndTime);
+
+        context.startRendering()
+            .then(createFilterVerifier(
+                should, createBandpassFilter, 3.1471e-5, parameters,
+                b.getChannelData(0),
+                'Output of bandpass filter with detune automation'))
+            .then(() => task.done());
+      });
+
+      // Automate all of the filter parameters at once.  This is a basic check
+      // that everything is working.  A peaking filter is used because it uses
+      // all of the parameters.
+      audit.define('automate-all', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+        let graph = configureGraph(context, 10 * 440);
+        let f = graph.filter;
+        let b = graph.source;
+
+        // Sweep all of the filter parameters.  These are pretty much arbitrary.
+        let parameters = {
+          freq: [8000, 100],
+          Q: [f.Q.value, .0001],
+          gain: [f.gain.value, 20],
+          detune: [2400, -2400]
+        };
+
+        f.type = 'peaking';
+        // Set starting points for all parameters of the filter.  Start at 10
+        // kHz for the center frequency, and the defaults for Q and gain.
+        f.frequency.setValueAtTime(parameters.freq[0], 0);
+        f.Q.setValueAtTime(parameters.Q[0], 0);
+        f.gain.setValueAtTime(parameters.gain[0], 0);
+        f.detune.setValueAtTime(parameters.detune[0], 0);
+
+        // Linear ramp each parameter
+        f.frequency.linearRampToValueAtTime(
+            parameters.freq[1], automationEndTime);
+        f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);
+        f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);
+        f.detune.linearRampToValueAtTime(
+            parameters.detune[1], automationEndTime);
+
+        context.startRendering()
+            .then(createFilterVerifier(
+                should, createPeakingFilter, 6.2907e-4, parameters,
+                b.getChannelData(0),
+                'Output of peaking filter with automation of all parameters'))
+            .then(() => task.done());
+      });
+
+      // Test that modulation of the frequency parameter of the filter works.  A
+      // sinusoid of 440 Hz is the test signal that is applied to a bandpass
+      // biquad filter.  The frequency parameter of the filter is modulated by a
+      // sinusoid at 103 Hz, and the frequency modulation varies from 116 to 412
+      // Hz.  (This test was taken from the description in
+      // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355)
+      audit.define('modulation', (task, should) => {
+        let context =
+            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+        // Create a graph with the sinusoidal source at 440 Hz as the input to a
+        // biquad filter.
+        let graph = configureGraph(context, 440);
+        let f = graph.filter;
+        let b = graph.source;
+
+        f.type = 'bandpass';
+        f.Q.value = 5;
+        f.frequency.value = 264;
+
+        // Create the modulation source, a sinusoid with frequency 103 Hz and
+        // amplitude 148.  (The amplitude of 148 is added to the filter's
+        // frequency value of 264 to produce a sinusoidal modulation of the
+        // frequency parameter from 116 to 412 Hz.)
+        let mod = context.createBufferSource();
+        let mbuffer =
+            context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+        let d = mbuffer.getChannelData(0);
+        let omega = 2 * Math.PI * 103 / sampleRate;
+        for (let k = 0; k < d.length; ++k) {
+          d[k] = 148 * Math.sin(omega * k);
+        }
+        mod.buffer = mbuffer;
+
+        mod.connect(f.frequency);
+
+        mod.start();
+        context.startRendering()
+            .then(function(resultBuffer) {
+              let actual = resultBuffer.getChannelData(0);
+              // Compute the filter coefficients using the mod sine wave
+
+              let endFrame = Math.ceil(renderDuration * sampleRate);
+              let nCoef = endFrame;
+              let b0 = new Float64Array(nCoef);
+              let b1 = new Float64Array(nCoef);
+              let b2 = new Float64Array(nCoef);
+              let a1 = new Float64Array(nCoef);
+              let a2 = new Float64Array(nCoef);
+
+              // Generate the filter coefficients when the frequency varies from
+              // 116 to 248 Hz using the 103 Hz sinusoid.
+              for (let k = 0; k < nCoef; ++k) {
+                let freq = f.frequency.value + d[k];
+                let c = createBandpassFilter(
+                    freq / (sampleRate / 2), f.Q.value, f.gain.value);
+                b0[k] = c.b0;
+                b1[k] = c.b1;
+                b2[k] = c.b2;
+                a1[k] = c.a1;
+                a2[k] = c.a2;
+              }
+              reference = timeVaryingFilter(
+                  b.getChannelData(0),
+                  {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2});
+
+              should(
+                  actual,
+                  'Output of bandpass filter with sinusoidal modulation of bandpass center frequency')
+                  .beCloseToArray(reference, {absoluteThreshold: 3.9787e-5});
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html
new file mode 100644
index 0000000..166aa9b
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-bandpass.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad bandpass filter.'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 0, gain: 1},
+              {cutoff: 1, q: 0, gain: 1},
+              {cutoff: 0.5, q: 0, gain: 1},
+              {cutoff: 0.25, q: 1, gain: 1},
+            ];
+
+            createTestAndRun(context, 'bandpass', {
+              should: should,
+              threshold: 2.2501e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html
new file mode 100644
index 0000000..c4f7c07
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Basic BiquadFilterNode Properties
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let sampleRate = 48000;
+      let testFrames = 100;
+
+      // Global context that can be used by the individual tasks. It must be
+      // defined by the initialize task.
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        should(() => {
+          context = new OfflineAudioContext(1, testFrames, sampleRate);
+        }, 'Initialize context for testing').notThrow();
+        task.done();
+      });
+
+      audit.define('existence', (task, should) => {
+        should(context.createBiquadFilter, 'context.createBiquadFilter')
+            .exist();
+        task.done();
+      });
+
+      audit.define('parameters', (task, should) => {
+        // Create a really simple IIR filter. Doesn't much matter what.
+        let coef = Float32Array.from([1]);
+
+        let f = context.createBiquadFilter(coef, coef);
+
+        should(f.numberOfInputs, 'numberOfInputs').beEqualTo(1);
+        should(f.numberOfOutputs, 'numberOfOutputs').beEqualTo(1);
+        should(f.channelCountMode, 'channelCountMode').beEqualTo('max');
+        should(f.channelInterpretation, 'channelInterpretation')
+            .beEqualTo('speakers');
+
+        task.done();
+      });
+
+      audit.define('exceptions-createBiquadFilter', (task, should) => {
+        should(function() {
+          // Two args are required.
+          context.createBiquadFilter();
+        }, 'createBiquadFilter()').notThrow();
+
+        task.done();
+      });
+
+      audit.define('exceptions-getFrequencyData', (task, should) => {
+        // Create a really simple IIR filter. Doesn't much matter what.
+        let coef = Float32Array.from([1]);
+
+        let f = context.createBiquadFilter(coef, coef);
+
+        should(
+            function() {
+              // frequencyHz can't be null.
+              f.getFrequencyResponse(
+                  null, new Float32Array(1), new Float32Array(1));
+            },
+            'getFrequencyResponse(' +
+                'null, ' +
+                'new Float32Array(1), ' +
+                'new Float32Array(1))')
+            .throw('TypeError');
+
+        should(
+            function() {
+              // magResponse can't be null.
+              f.getFrequencyResponse(
+                  new Float32Array(1), null, new Float32Array(1));
+            },
+            'getFrequencyResponse(' +
+                'new Float32Array(1), ' +
+                'null, ' +
+                'new Float32Array(1))')
+            .throw('TypeError');
+
+        should(
+            function() {
+              // phaseResponse can't be null.
+              f.getFrequencyResponse(
+                  new Float32Array(1), new Float32Array(1), null);
+            },
+            'getFrequencyResponse(' +
+                'new Float32Array(1), ' +
+                'new Float32Array(1), ' +
+                'null)')
+            .throw('TypeError');
+
+        should(
+            function() {
+              // magResponse array must the same length as frequencyHz
+              f.getFrequencyResponse(
+                  new Float32Array(10), new Float32Array(1),
+                  new Float32Array(20));
+            },
+            'getFrequencyResponse(' +
+                'new Float32Array(10), ' +
+                'new Float32Array(1), ' +
+                'new Float32Array(20))')
+            .throw('InvalidAccessError');
+
+        should(
+            function() {
+              // phaseResponse array must be the same length as frequencyHz
+              f.getFrequencyResponse(
+                  new Float32Array(10), new Float32Array(20),
+                  new Float32Array(1));
+            },
+            'getFrequencyResponse(' +
+                'new Float32Array(10), ' +
+                'new Float32Array(20), ' +
+                'new Float32Array(1))')
+            .throw('InvalidAccessError');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html
new file mode 100644
index 0000000..83f057f
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html
@@ -0,0 +1,335 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test BiquadFilter getFrequencyResponse() functionality
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // Test the frequency response of a biquad filter.  We compute the
+      // frequency response for a simple peaking biquad filter and compare it
+      // with the expected frequency response.  The actual filter used doesn't
+      // matter since we're testing getFrequencyResponse and not the actual
+      // filter output. The filters are extensively tested in other biquad
+      // tests.
+
+      // The magnitude response of the biquad filter.
+      let magResponse;
+
+      // The phase response of the biquad filter.
+      let phaseResponse;
+
+      // Number of frequency samples to take.
+      let numberOfFrequencies = 1000;
+
+      // The filter parameters.
+      let filterCutoff = 1000;  // Hz.
+      let filterQ = 1;
+      let filterGain = 5;  // Decibels.
+
+      // The maximum allowed error in the magnitude response.
+      let maxAllowedMagError = 9.775e-7;
+
+      // The maximum allowed error in the phase response.
+      let maxAllowedPhaseError = 5.4187e-8;
+
+      // The magnitudes and phases of the reference frequency response.
+      let expectedMagnitudes;
+      let expectedPhases;
+
+      // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1
+      // corresponding to the Nyquist frequency.
+      function normalizedFrequency(freqHz, sampleRate) {
+        let nyquist = sampleRate / 2;
+        return freqHz / nyquist;
+      }
+
+      // Get the filter response at a (normalized) frequency |f| for the filter
+      // with coefficients |coef|.
+      function getResponseAt(coef, f) {
+        let b0 = coef.b0;
+        let b1 = coef.b1;
+        let b2 = coef.b2;
+        let a1 = coef.a1;
+        let a2 = coef.a2;
+
+        // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
+        //
+        // Compute H(exp(i * pi * f)).  No native complex numbers in javascript,
+        // so break H(exp(i * pi * // f)) in to the real and imaginary parts of
+        // the numerator and denominator.  Let omega = pi * f. Then the
+        // numerator is
+        //
+        // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) +
+        // b2 * sin(2 * omega))
+        //
+        // and the denominator is
+        //
+        // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2
+        // * sin(2 * omega))
+        //
+        // Compute the magnitude and phase from the real and imaginary parts.
+
+        let omega = Math.PI * f;
+        let numeratorReal =
+            b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
+        let numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
+        let denominatorReal =
+            1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
+        let denominatorImag =
+            -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));
+
+        let magnitude = Math.sqrt(
+            (numeratorReal * numeratorReal + numeratorImag * numeratorImag) /
+            (denominatorReal * denominatorReal +
+             denominatorImag * denominatorImag));
+        let phase = Math.atan2(numeratorImag, numeratorReal) -
+            Math.atan2(denominatorImag, denominatorReal);
+
+        if (phase >= Math.PI) {
+          phase -= 2 * Math.PI;
+        } else if (phase <= -Math.PI) {
+          phase += 2 * Math.PI;
+        }
+
+        return {magnitude: magnitude, phase: phase};
+      }
+
+      // Compute the reference frequency response for the biquad filter |filter|
+      // at the frequency samples given by |frequencies|.
+      function frequencyResponseReference(filter, frequencies) {
+        let sampleRate = filter.context.sampleRate;
+        let normalizedFreq =
+            normalizedFrequency(filter.frequency.value, sampleRate);
+        let filterCoefficients = createFilter(
+            filter.type, normalizedFreq, filter.Q.value, filter.gain.value);
+
+        let magnitudes = [];
+        let phases = [];
+
+        for (let k = 0; k < frequencies.length; ++k) {
+          let response = getResponseAt(
+              filterCoefficients,
+              normalizedFrequency(frequencies[k], sampleRate));
+          magnitudes.push(response.magnitude);
+          phases.push(response.phase);
+        }
+
+        return {magnitudes: magnitudes, phases: phases};
+      }
+
+      // Compute a set of linearly spaced frequencies.
+      function createFrequencies(nFrequencies, sampleRate) {
+        let frequencies = new Float32Array(nFrequencies);
+        let nyquist = sampleRate / 2;
+        let freqDelta = nyquist / nFrequencies;
+
+        for (let k = 0; k < nFrequencies; ++k) {
+          frequencies[k] = k * freqDelta;
+        }
+
+        return frequencies;
+      }
+
+      function linearToDecibels(x) {
+        if (x) {
+          return 20 * Math.log(x) / Math.LN10;
+        } else {
+          return -1000;
+        }
+      }
+
+      // Look through the array and find any NaN or infinity. Returns the index
+      // of the first occurence or -1 if none.
+      function findBadNumber(signal) {
+        for (let k = 0; k < signal.length; ++k) {
+          if (!isValidNumber(signal[k])) {
+            return k;
+          }
+        }
+        return -1;
+      }
+
+      // Compute absolute value of the difference between phase angles, taking
+      // into account the wrapping of phases.
+      function absolutePhaseDifference(x, y) {
+        let diff = Math.abs(x - y);
+
+        if (diff > Math.PI) {
+          diff = 2 * Math.PI - diff;
+        }
+        return diff;
+      }
+
+      // Compare the frequency response with our expected response.
+      function compareResponses(
+          should, filter, frequencies, magResponse, phaseResponse) {
+        let expectedResponse = frequencyResponseReference(filter, frequencies);
+
+        expectedMagnitudes = expectedResponse.magnitudes;
+        expectedPhases = expectedResponse.phases;
+
+        let n = magResponse.length;
+        let badResponse = false;
+
+        let maxMagError = -1;
+        let maxMagErrorIndex = -1;
+
+        let k;
+        let hasBadNumber;
+
+        hasBadNumber = findBadNumber(magResponse);
+        badResponse = !should(
+                           hasBadNumber >= 0 ? 1 : 0,
+                           'Number of non-finite values in magnitude response')
+                           .beEqualTo(0);
+
+        hasBadNumber = findBadNumber(phaseResponse);
+        badResponse = !should(
+                           hasBadNumber >= 0 ? 1 : 0,
+                           'Number of non-finte values in phase response')
+                           .beEqualTo(0);
+
+        // These aren't testing the implementation itself.  Instead, these are
+        // sanity checks on the reference.  Failure here does not imply an error
+        // in the implementation.
+        hasBadNumber = findBadNumber(expectedMagnitudes);
+        badResponse =
+            !should(
+                 hasBadNumber >= 0 ? 1 : 0,
+                 'Number of non-finite values in the expected magnitude response')
+                 .beEqualTo(0);
+
+        hasBadNumber = findBadNumber(expectedPhases);
+        badResponse =
+            !should(
+                 hasBadNumber >= 0 ? 1 : 0,
+                 'Number of non-finite values in expected phase response')
+                 .beEqualTo(0);
+
+        // If we found a NaN or infinity, the following tests aren't very
+        // helpful, especially for NaN. We run them anyway, after printing a
+        // warning message.
+        should(
+            !badResponse,
+            'Actual and expected results contained only finite values')
+            .beTrue();
+
+        for (k = 0; k < n; ++k) {
+          let error = Math.abs(
+              linearToDecibels(magResponse[k]) -
+              linearToDecibels(expectedMagnitudes[k]));
+          if (error > maxMagError) {
+            maxMagError = error;
+            maxMagErrorIndex = k;
+          }
+        }
+
+        should(
+            linearToDecibels(maxMagError),
+            'Max error (' + linearToDecibels(maxMagError) +
+                ' dB) of magnitude response at frequency ' +
+                frequencies[maxMagErrorIndex] + ' Hz')
+            .beLessThanOrEqualTo(linearToDecibels(maxAllowedMagError));
+        let maxPhaseError = -1;
+        let maxPhaseErrorIndex = -1;
+
+        for (k = 0; k < n; ++k) {
+          let error =
+              absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
+          if (error > maxPhaseError) {
+            maxPhaseError = error;
+            maxPhaseErrorIndex = k;
+          }
+        }
+
+        should(
+            radToDegree(maxPhaseError),
+            'Max error (' + radToDegree(maxPhaseError) +
+                ' deg) in phase response at frequency ' +
+                frequencies[maxPhaseErrorIndex] + ' Hz')
+            .beLessThanOrEqualTo(radToDegree(maxAllowedPhaseError));
+      }
+
+      function radToDegree(rad) {
+        // Radians to degrees
+        return rad * 180 / Math.PI;
+      }
+
+      audit.define(
+          {label: 'test', description: 'Biquad frequency response'},
+          function(task, should) {
+            context = new AudioContext();
+
+            filter = context.createBiquadFilter();
+
+            // Arbitrarily test a peaking filter, but any kind of filter can be
+            // tested.
+            filter.type = 'peaking';
+            filter.frequency.value = filterCutoff;
+            filter.Q.value = filterQ;
+            filter.gain.value = filterGain;
+
+            let frequencies =
+                createFrequencies(numberOfFrequencies, context.sampleRate);
+            magResponse = new Float32Array(numberOfFrequencies);
+            phaseResponse = new Float32Array(numberOfFrequencies);
+
+            filter.getFrequencyResponse(
+                frequencies, magResponse, phaseResponse);
+            compareResponses(
+                should, filter, frequencies, magResponse, phaseResponse);
+
+            task.done();
+          });
+
+      audit.define(
+          {
+            label: 'getFrequencyResponse',
+            description: 'Test out-of-bounds frequency values'
+          },
+          (task, should) => {
+            let context = new OfflineAudioContext(1, 1, sampleRate);
+            let filter = new BiquadFilterNode(context);
+
+            // Frequencies to test.  These are all outside the valid range of
+            // frequencies of 0 to Nyquist.
+            let freq = new Float32Array(2);
+            freq[0] = -1;
+            freq[1] = context.sampleRate / 2 + 1;
+
+            let mag = new Float32Array(freq.length);
+            let phase = new Float32Array(freq.length);
+
+            filter.getFrequencyResponse(freq, mag, phase);
+
+            // Verify that the returned magnitude and phase entries are alL NaN
+            // since the frequencies are outside the valid range
+            for (let k = 0; k < mag.length; ++k) {
+              should(mag[k],
+                  'Magnitude response at frequency ' + freq[k])
+                  .beNaN();
+            }
+
+            for (let k = 0; k < phase.length; ++k) {
+              should(phase[k],
+                  'Phase response at frequency ' + freq[k])
+                  .beNaN();
+            }
+
+            task.done();
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html
new file mode 100644
index 0000000..45c335b
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-highpass.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad highpass filter'},
+          function(task, should) {
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 1, gain: 1},
+              {cutoff: 1, q: 1, gain: 1},
+              {cutoff: 0.25, q: 1, gain: 1},
+            ];
+
+            createTestAndRun(context, 'highpass', {
+              should: should,
+              threshold: 1.5487e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html
new file mode 100644
index 0000000..345195f
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-highshelf.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad highshelf filter'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 10, gain: 10},
+              {cutoff: 1, q: 10, gain: 10},
+              {cutoff: 0.25, q: 10, gain: 10},
+            ];
+
+            createTestAndRun(context, 'highshelf', {
+              should: should,
+              threshold: 6.2577e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html
new file mode 100644
index 0000000..d20786e
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-lowpass.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad lowpass filter'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 1, gain: 1},
+              {cutoff: 1, q: 1, gain: 1},
+              {cutoff: 0.25, q: 1, gain: 1},
+              {cutoff: 0.25, q: 1, gain: 1, detune: 100},
+              {cutoff: 0.01, q: 1, gain: 1, detune: -200},
+            ];
+
+            createTestAndRun(context, 'lowpass', {
+              should: should,
+              threshold: 9.7869e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html
new file mode 100644
index 0000000..ab76cef
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-lowshelf.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad lowshelf filter'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 10, gain: 10},
+              {cutoff: 1, q: 10, gain: 10},
+              {cutoff: 0.25, q: 10, gain: 10},
+            ];
+
+            createTestAndRun(context, 'lowshelf', {
+              should: should,
+              threshold: 3.8349e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html
new file mode 100644
index 0000000..98e6e6e
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-notch.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad notch filter'},
+          function(task, should) {
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            let filterParameters = [
+              {cutoff: 0, q: 10, gain: 1},
+              {cutoff: 1, q: 10, gain: 1},
+              {cutoff: .5, q: 0, gain: 1},
+              {cutoff: 0.25, q: 10, gain: 1},
+            ];
+
+            createTestAndRun(context, 'notch', {
+              should: should,
+              threshold: 1.9669e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html
new file mode 100644
index 0000000..90b7c15
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-peaking.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+    <script src="/webaudio/resources/biquad-testing.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Biquad peaking filter'},
+          function(task, should) {
+
+            window.jsTestIsAsync = true;
+
+            // Create offline audio context.
+            let context = new OfflineAudioContext(
+                2, sampleRate * renderLengthSeconds, sampleRate);
+
+            // The filters we want to test.
+            let filterParameters = [
+              {cutoff: 0, q: 10, gain: 10},
+              {cutoff: 1, q: 10, gain: 10},
+              {cutoff: .5, q: 0, gain: 10},
+              {cutoff: 0.25, q: 10, gain: 10},
+            ];
+
+            createTestAndRun(context, 'peaking', {
+              should: should,
+              threshold: 5.8234e-8,
+              filterParameters: filterParameters
+            }).then(task.done.bind(task));
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html
new file mode 100644
index 0000000..3141bf7
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Biquad Tail Output
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // A high sample rate shows the issue more clearly.
+      let sampleRate = 192000;
+      // Some short duration because we don't need to run the test for very
+      // long.
+      let testDurationSec = 0.5;
+      let testDurationFrames = testDurationSec * sampleRate;
+
+      // Amplitude experimentally determined to give a biquad output close to 1.
+      // (No attempt was made to produce exactly 1; it's not needed.)
+      let sourceAmplitude = 100;
+
+      // The output of the biquad filter should not change by more than this
+      // much between output samples.  Threshold was determined experimentally.
+      let glitchThreshold = 0.012968;
+
+      // Test that a Biquad filter doesn't have it's output terminated because
+      // the input has gone away.  Generally, when a source node is finished, it
+      // disconnects itself from any downstream nodes.  This is the correct
+      // behavior.  Nodes that have no inputs (disconnected) are generally
+      // assumed to output zeroes.  This is also desired behavior.  However,
+      // biquad filters have memory so they should not suddenly output zeroes
+      // when the input is disconnected.  This test checks to see if the output
+      // doesn't suddenly change to zero.
+      audit.define(
+          {label: 'test', description: 'Biquad Tail Output'},
+          function(task, should) {
+            let context =
+                new OfflineAudioContext(1, testDurationFrames, sampleRate);
+
+            // Create an impulse source.
+            let buffer = context.createBuffer(1, 1, context.sampleRate);
+            buffer.getChannelData(0)[0] = sourceAmplitude;
+            let source = context.createBufferSource();
+            source.buffer = buffer;
+
+            // Create the biquad filter. It doesn't really matter what kind, so
+            // the default filter type and parameters is fine.  Connect the
+            // source to it.
+            let biquad = context.createBiquadFilter();
+            source.connect(biquad);
+            biquad.connect(context.destination);
+
+            source.start();
+
+            context.startRendering().then(function(result) {
+              // There should be no large discontinuities in the output
+              should(result.getChannelData(0), 'Biquad output')
+                  .notGlitch(glitchThreshold);
+              task.done();
+            })
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html b/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html
new file mode 100644
index 0000000..7e71d07
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquadfilternode-basic.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      audit.define(
+          {label: 'test', description: 'Basic tests for BiquadFilterNode'},
+          function(task, should) {
+
+            let context = new AudioContext();
+            let filter = context.createBiquadFilter();
+
+            should(filter.numberOfInputs, 'Number of inputs').beEqualTo(1);
+
+            should(filter.numberOfOutputs, 'Number of outputs').beEqualTo(1);
+
+            should(filter.type, 'Default filter type').beEqualTo('lowpass');
+
+            should(filter.frequency.value, 'Default frequency value')
+                .beEqualTo(350);
+
+            should(filter.Q.value, 'Default Q value').beEqualTo(1);
+
+            should(filter.gain.value, 'Default gain value').beEqualTo(0);
+
+            // Check that all legal filter types can be set.
+            let filterTypeArray = [
+              {type: 'lowpass'}, {type: 'highpass'}, {type: 'bandpass'},
+              {type: 'lowshelf'}, {type: 'highshelf'}, {type: 'peaking'},
+              {type: 'notch'}, {type: 'allpass'}
+            ];
+
+            for (let i = 0; i < filterTypeArray.length; ++i) {
+              should(
+                  () => filter.type = filterTypeArray[i].type,
+                  'Setting filter.type to ' + filterTypeArray[i].type)
+                  .notThrow();
+              should(filter.type, 'Filter type is')
+                  .beEqualTo(filterTypeArray[i].type);
+            }
+
+
+            // Check that numerical values are no longer supported
+            filter.type = 99;
+            should(filter.type, 'Setting filter.type to (invalid) 99')
+                .notBeEqualTo(99);
+
+            task.done();
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html b/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html
new file mode 100644
index 0000000..e63479f
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: BiquadFilter
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'BiquadFilterNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'BiquadFilterNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'type', value: 'lowpass'}, {name: 'Q', value: 1},
+          {name: 'detune', value: 0}, {name: 'frequency', value: 350},
+          {name: 'gain', value: 0.0}
+        ]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'BiquadFilterNode');
+        task.done();
+      });
+
+      audit.define('construct with options', (task, should) => {
+        let node;
+        let options = {
+          type: 'highpass',
+          frequency: 512,
+          detune: 1,
+          Q: 5,
+          gain: 3,
+        };
+
+        should(
+            () => {
+              node = new BiquadFilterNode(context, options);
+            },
+            'node = new BiquadFilterNode(..., ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        // Test that attributes are set according to the option values.
+        should(node.type, 'node.type').beEqualTo(options.type);
+        should(node.frequency.value, 'node.frequency.value')
+            .beEqualTo(options.frequency);
+        should(node.detune.value, 'node.detuen.value')
+            .beEqualTo(options.detune);
+        should(node.Q.value, 'node.Q.value').beEqualTo(options.Q);
+        should(node.gain.value, 'node.gain.value').beEqualTo(options.gain);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html b/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html
new file mode 100644
index 0000000..d54bc0b
--- /dev/null
+++ b/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html
@@ -0,0 +1,288 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      biquad-bandpass.html
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/biquad-filters.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let audit = Audit.createTaskRunner();
+
+      // In the tests below, the initial values are not important, except that
+      // we wanted them to be all different so that the output contains
+      // different values for the first few samples.  Otherwise, the actual
+      // values don't really matter.  A peaking filter is used because the
+      // frequency, Q, gain, and detune parameters are used by this filter.
+      //
+      // Also, for the changeList option, the times and new values aren't really
+      // important.  They just need to change so that we can verify that the
+      // outputs from the .value setter still matches the output from the
+      // corresponding setValueAtTime.
+      audit.define(
+          {label: 'Test 0', description: 'No dezippering for frequency'},
+          (task, should) => {
+            doTest(should, {
+              paramName: 'frequency',
+              initializer: {type: 'peaking', Q: 1, gain: 5},
+              changeList:
+                  [{quantum: 2, newValue: 800}, {quantum: 7, newValue: 200}],
+              threshold: 3.0399e-6
+            }).then(() => task.done());
+          });
+
+      audit.define(
+          {label: 'Test 1', description: 'No dezippering for detune'},
+          (task, should) => {
+            doTest(should, {
+              paramName: 'detune',
+              initializer:
+                  {type: 'peaking', frequency: 400, Q: 3, detune: 33, gain: 10},
+              changeList:
+                  [{quantum: 2, newValue: 1000}, {quantum: 5, newValue: -400}],
+              threshold: 4.0532e-6
+            }).then(() => task.done());
+          });
+
+      audit.define(
+          {label: 'Test 2', description: 'No dezippering for Q'},
+          (task, should) => {
+            doTest(should, {
+              paramName: 'Q',
+              initializer: {type: 'peaking', Q: 5},
+              changeList:
+                  [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
+            }).then(() => task.done());
+          });
+
+      audit.define(
+          {label: 'Test 3', description: 'No dezippering for gain'},
+          (task, should) => {
+            doTest(should, {
+              paramName: 'gain',
+              initializer: {type: 'peaking', gain: 1},
+              changeList:
+                  [{quantum: 2, newValue: 5}, {quantum: 6, newValue: -.3}],
+              threshold: 1.9074e-6
+            }).then(() => task.done());
+          });
+
+      // This test compares the filter output against a JS implementation of the
+      // filter.  We're only testing a change in the frequency for a lowpass
+      // filter.  This assumes we don't need to test other AudioParam changes
+      // with JS code because any mistakes would be exposed in the tests above.
+      audit.define(
+          {
+            label: 'Test 4',
+            description: 'No dezippering of frequency vs JS filter'
+          },
+          (task, should) => {
+            // Channel 0 is the source, channel 1 is the filtered output.
+            let context = new OfflineAudioContext(2, 2048, 16384);
+
+            let merger = new ChannelMergerNode(
+                context, {numberOfInputs: context.destination.channelCount});
+            merger.connect(context.destination);
+
+            let src = new OscillatorNode(context);
+            let f = new BiquadFilterNode(context, {type: 'lowpass'});
+
+            // Remember the initial filter parameters.
+            let initialFilter = {
+              type: f.type,
+              frequency: f.frequency.value,
+              gain: f.gain.value,
+              detune: f.detune.value,
+              Q: f.Q.value
+            };
+
+            src.connect(merger, 0, 0);
+            src.connect(f).connect(merger, 0, 1);
+
+            // Apply the filter change at frame |changeFrame| with a new
+            // frequency value of |newValue|.
+            let changeFrame = 2 * RENDER_QUANTUM_FRAMES;
+            let newValue = 750;
+
+            context.suspend(changeFrame / context.sampleRate)
+                .then(() => f.frequency.value = newValue)
+                .then(() => context.resume());
+
+            src.start();
+
+            context.startRendering()
+                .then(audio => {
+                  let signal = audio.getChannelData(0);
+                  let actual = audio.getChannelData(1);
+
+                  // Get initial filter coefficients and updated coefficients
+                  let nyquistFreq = context.sampleRate / 2;
+                  let initialCoef = createFilter(
+                      initialFilter.type, initialFilter.frequency / nyquistFreq,
+                      initialFilter.Q, initialFilter.gain);
+
+                  let finalCoef = createFilter(
+                      f.type, f.frequency.value / nyquistFreq, f.Q.value,
+                      f.gain.value);
+
+                  let expected = new Float32Array(signal.length);
+
+                  // Filter the initial part of the signal.
+                  expected[0] =
+                      filterSample(signal[0], initialCoef, 0, 0, 0, 0);
+                  expected[1] = filterSample(
+                      signal[1], initialCoef, expected[0], 0, signal[0], 0);
+
+                  for (let k = 2; k < changeFrame; ++k) {
+                    expected[k] = filterSample(
+                        signal[k], initialCoef, expected[k - 1],
+                        expected[k - 2], signal[k - 1], signal[k - 2]);
+                  }
+
+                  // Filter the rest of the input with the new coefficients
+                  for (let k = changeFrame; k < signal.length; ++k) {
+                    expected[k] = filterSample(
+                        signal[k], finalCoef, expected[k - 1], expected[k - 2],
+                        signal[k - 1], signal[k - 2]);
+                  }
+
+                  // The JS filter should match the actual output.
+                  let match =
+                      should(actual, 'Output from ' + f.type + ' filter')
+                          .beCloseToArray(
+                              expected, {absoluteThreshold: 4.7684e-7});
+                  should(match, 'Output matches JS filter results').beTrue();
+                })
+                .then(() => task.done());
+          });
+
+      audit.define(
+          {label: 'Test 5', description: 'Test with modulation'},
+          (task, should) => {
+            doTest(should, {
+              prefix: 'Modulation: ',
+              paramName: 'frequency',
+              initializer: {type: 'peaking', Q: 5, gain: 5},
+              modulation: true,
+              changeList:
+                  [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
+            }).then(() => task.done());
+
+          });
+
+      audit.run();
+
+      // Run test, returning the promise from startRendering. |options|
+      // specifies the parameters for the test. |options.paramName| is the name
+      // of the AudioParam of the filter that is being tested.
+      // |options.initializer| is the initial value to be used in constructing
+      // the filter. |options.changeList| is an array consisting of dictionary
+      // with two members: |quantum| is the rendering quantum at which time we
+      // want to change the AudioParam value, and |newValue| is the value to be
+      // used.
+      function doTest(should, options) {
+        let paramName = options.paramName;
+        let newValue = options.newValue;
+        let prefix = options.prefix || '';
+
+        // Create offline audio context.  The sample rate should be a power of
+        // two to eliminate any round-off errors in computing the time at which
+        // to suspend the context for the parameter change.  The length is
+        // fairly arbitrary as long as it's big enough to the changeList
+        // values. There are two channels:  channel 0 is output for the filter
+        // under test, and channel 1 is the output of referencef filter.
+        let context = new OfflineAudioContext(2, 2048, 16384);
+
+        let merger = new ChannelMergerNode(
+            context, {numberOfInputs: context.destination.channelCount});
+        merger.connect(context.destination);
+
+        let src = new OscillatorNode(context);
+
+        // |f0| is the filter under test that will have its AudioParam value
+        // changed. |f1| is the reference filter that uses setValueAtTime to
+        // update the AudioParam value.
+        let f0 = new BiquadFilterNode(context, options.initializer);
+        let f1 = new BiquadFilterNode(context, options.initializer);
+
+        src.connect(f0).connect(merger, 0, 0);
+        src.connect(f1).connect(merger, 0, 1);
+
+        // Modulate the AudioParam with an input signal, if requested.
+        if (options.modulation) {
+          // The modulation signal is a sine wave with amplitude 1/3 the cutoff
+          // frequency of the test filter.  The amplitude is fairly arbitrary,
+          // but we want it to be a significant fraction of the cutoff so that
+          // the cutoff varies quite a bit in the test.
+          let mod =
+              new OscillatorNode(context, {type: 'sawtooth', frequency: 1000});
+          let modGain = new GainNode(context, {gain: f0.frequency.value / 3});
+          mod.connect(modGain);
+          modGain.connect(f0[paramName]);
+          modGain.connect(f1[paramName]);
+          mod.start();
+        }
+        // Output a message showing where we're starting from.
+        should(f0[paramName].value, prefix + `At time 0, ${paramName}`)
+            .beEqualTo(f0[paramName].value);
+
+        // Schedule all of the desired changes from |changeList|.
+        options.changeList.forEach(change => {
+          let changeTime =
+              change.quantum * RENDER_QUANTUM_FRAMES / context.sampleRate;
+          let value = change.newValue;
+
+          // Just output a message to show what we're doing.
+          should(value, prefix + `At time ${changeTime}, ${paramName}`)
+              .beEqualTo(value);
+
+          // Update the AudioParam value of each filter using setValueAtTime or
+          // the value setter.
+          f1[paramName].setValueAtTime(value, changeTime);
+          context.suspend(changeTime)
+              .then(() => f0[paramName].value = value)
+              .then(() => context.resume());
+        });
+
+        src.start();
+
+        return context.startRendering().then(audio => {
+          let actual = audio.getChannelData(0);
+          let expected = audio.getChannelData(1);
+
+          // The output from both filters MUST match exactly if dezippering has
+          // been properly removed.
+          let match = should(actual, `${prefix}Output from ${paramName} setter`)
+                          .beCloseToArray(
+                              expected, {absoluteThreshold: options.threshold});
+
+          // Just an extra message saying that what we're comparing, to make the
+          // output clearer. (Not really neceesary, but nice.)
+          should(
+              match,
+              `${prefix}Output from ${
+                                      paramName
+                                    } setter matches setValueAtTime output`)
+              .beTrue();
+        });
+      }
+
+      // Filter one sample:
+      //
+      //   y[n] = b0 * x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
+      //
+      // where |x| is x[n], |xn1| is x[n-1], |xn2| is x[n-2], |yn1| is y[n-1],
+      // and |yn2| is y[n-2].  |coef| is a dictonary of the filter coefficients
+      // |b0|, |b1|, |b2|, |a1|, and |a2|.
+      function filterSample(x, coef, yn1, yn2, xn1, xn2) {
+        return coef.b0 * x + coef.b1 * xn1 + coef.b2 * xn2 - coef.a1 * yn1 -
+            coef.a2 * yn2;
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-channelmergernode-interface/ctor-channelmerger.html b/webaudio/the-audio-api/the-channelmergernode-interface/ctor-channelmerger.html
new file mode 100644
index 0000000..115bd99
--- /dev/null
+++ b/webaudio/the-audio-api/the-channelmergernode-interface/ctor-channelmerger.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: ChannelMerger
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'ChannelMergerNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node =
+            testDefaultConstructor(should, 'ChannelMergerNode', context, {
+              prefix: prefix,
+              numberOfInputs: 6,
+              numberOfOutputs: 1,
+              channelCount: 1,
+              channelCountMode: 'explicit',
+              channelInterpretation: 'speakers'
+            });
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'ChannelMergerNode', {
+          channelCount:
+              {value: 1, isFixed: true, errorType: 'InvalidStateError'},
+          channelCountMode: {
+            value: 'explicit',
+            isFixed: true,
+            errorType: 'InvalidStateError'
+          }
+        });
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+        let options = {
+          numberOfInputs: 3,
+          numberOfOutputs: 9,
+          channelInterpretation: 'discrete'
+        };
+
+        should(
+            () => {
+              node = new ChannelMergerNode(context, options);
+            },
+            'node1 = new ChannelMergerNode(context, ' +
+                JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(node.numberOfInputs, 'node1.numberOfInputs')
+            .beEqualTo(options.numberOfInputs);
+        should(node.numberOfOutputs, 'node1.numberOfOutputs').beEqualTo(1);
+        should(node.channelInterpretation, 'node1.channelInterpretation')
+            .beEqualTo(options.channelInterpretation);
+
+        options = {numberOfInputs: 99};
+        should(
+            () => {
+              node = new ChannelMergerNode(context, options);
+            },
+            'new ChannelMergerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('IndexSizeError');
+
+        options = {channelCount: 3};
+        should(
+            () => {
+              node = new ChannelMergerNode(context, options);
+            },
+            'new ChannelMergerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('InvalidStateError');
+
+        options = {channelCountMode: 'max'};
+        should(
+            () => {
+              node = new ChannelMergerNode(context, options);
+            },
+            'new ChannelMergerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('InvalidStateError');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-channelsplitternode-interface/ctor-channelsplitter.html b/webaudio/the-audio-api/the-channelsplitternode-interface/ctor-channelsplitter.html
new file mode 100644
index 0000000..7fa9d6f
--- /dev/null
+++ b/webaudio/the-audio-api/the-channelsplitternode-interface/ctor-channelsplitter.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: ChannelSplitter
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'ChannelSplitterNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        testDefaultConstructor(should, 'ChannelSplitterNode', context, {
+          prefix: 'node0',
+          numberOfInputs: 1,
+          numberOfOutputs: 6,
+          channelCount: 6,
+          channelCountMode: 'explicit',
+          channelInterpretation: 'discrete'
+        });
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'ChannelSplitterNode', {
+          channelCount:
+              {value: 6, isFixed: true, errorType: 'InvalidStateError'},
+          channelCountMode: {
+            value: 'explicit',
+            isFixed: true,
+          },
+          channelInterpretation: {
+            value: 'discrete',
+            isFixed: true,
+            errorType: 'InvalidStateError'
+          },
+        });
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+        let options = {
+          numberOfInputs: 3,
+          numberOfOutputs: 9,
+          channelInterpretation: 'discrete'
+        };
+
+        should(
+            () => {
+              node = new ChannelSplitterNode(context, options);
+            },
+            'node1 = new ChannelSplitterNode(context, ' +
+                JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(node.numberOfInputs, 'node1.numberOfInputs').beEqualTo(1);
+        should(node.numberOfOutputs, 'node1.numberOfOutputs')
+            .beEqualTo(options.numberOfOutputs);
+        should(node.channelInterpretation, 'node1.channelInterpretation')
+            .beEqualTo(options.channelInterpretation);
+
+        options = {numberOfOutputs: 99};
+        should(
+            () => {
+              node = new ChannelSplitterNode(context, options);
+            },
+            'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
+            .throw('IndexSizeError');
+
+        options = {channelCount: 3};
+        should(
+            () => {
+              node = new ChannelSplitterNode(context, options);
+            },
+            'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
+            .throw('InvalidStateError');
+
+        options = {channelCountMode: 'max'};
+        should(
+            () => {
+              node = new ChannelSplitterNode(context, options);
+            },
+            'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
+            .throw('InvalidStateError');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-constantsourcenode-interface/ctor-constantsource.html b/webaudio/the-audio-api/the-constantsourcenode-interface/ctor-constantsource.html
new file mode 100644
index 0000000..ea4a65e
--- /dev/null
+++ b/webaudio/the-audio-api/the-constantsourcenode-interface/ctor-constantsource.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: ConstantSource
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'ConstantSourceNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node =
+            testDefaultConstructor(should, 'ConstantSourceNode', context, {
+              prefix: prefix,
+              numberOfInputs: 0,
+              numberOfOutputs: 1,
+              channelCount: 2,
+              channelCountMode: 'max',
+              channelInterpretation: 'speakers'
+            });
+
+        testDefaultAttributes(
+            should, node, prefix, [{name: 'offset', value: 1}]);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html b/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html
new file mode 100644
index 0000000..cf81833
--- /dev/null
+++ b/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: Convolver
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'ConvolverNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'ConvolverNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'clamped-max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(
+            should, node, prefix,
+            [{name: 'normalize', value: true}, {name: 'buffer', value: null}]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'ConvolverNode', {
+          channelCount:
+              {value: 2, isFixed: true, errorType: 'NotSupportedError'},
+          channelCountMode: {
+            value: 'clamped-max',
+            isFixed: true,
+            errorType: 'NotSupportedError'
+          },
+        });
+        task.done();
+      });
+
+      audit.define('nullable buffer', (task, should) => {
+        let node;
+        let options = {buffer: null};
+
+        should(
+            () => {
+              node = new ConvolverNode(context, options);
+            },
+            'node1 = new ConvolverNode(c, ' + JSON.stringify(options))
+            .notThrow();
+
+        should(node.buffer, 'node1.buffer').beEqualTo(null);
+
+        task.done();
+      });
+
+      audit.define('construct with options', (task, should) => {
+        let buf = context.createBuffer(1, 1, context.sampleRate);
+        let options = {buffer: buf, disableNormalization: false};
+
+        let message =
+            'node = new ConvolverNode(c, ' + JSON.stringify(options) + ')';
+
+        let node;
+        should(() => {
+          node = new ConvolverNode(context, options);
+        }, message).notThrow();
+
+        should(node instanceof ConvolverNode, 'node1 instanceOf ConvolverNode')
+            .beEqualTo(true);
+        should(node.buffer === options.buffer, 'node1.buffer === <buf>')
+            .beEqualTo(true);
+        should(node.normalize, 'node1.normalize')
+            .beEqualTo(!options.disableNormalization);
+
+        options.buffer = null;
+        options.disableNormalization = true;
+
+        message =
+            'node2 = new ConvolverNode(, ' + JSON.stringify(options) + ')';
+
+        should(() => {
+          node = new ConvolverNode(context, options);
+        }, message).notThrow();
+        should(node.buffer, 'node2.buffer').beEqualTo(null);
+        should(node.normalize, 'node2.normalize')
+            .beEqualTo(!options.disableNormalization);
+
+        options.disableNormalization = false;
+        message = 'node3 = new ConvolverNode(context, ' +
+            JSON.stringify(options) + ')';
+
+        should(() => {
+          node = new ConvolverNode(context, options);
+        }, message).notThrow();
+        should(node.buffer, 'node3.buffer').beEqualTo(null);
+        should(node.normalize, 'node3.normalize')
+            .beEqualTo(!options.disableNormalization);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-delaynode-interface/ctor-delay.html b/webaudio/the-audio-api/the-delaynode-interface/ctor-delay.html
new file mode 100644
index 0000000..e7ccefc
--- /dev/null
+++ b/webaudio/the-audio-api/the-delaynode-interface/ctor-delay.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: Delay
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'DelayNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'DelayNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(
+            should, node, prefix, [{name: 'delayTime', value: 0}]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'DelayNode');
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+        let options = {
+          delayTime: 0.5,
+          maxDelayTime: 1.5,
+        };
+
+        should(
+            () => {
+              node = new DelayNode(context, options);
+            },
+            'node1 = new DelayNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(node.delayTime.value, 'node1.delayTime.value')
+            .beEqualTo(options.delayTime);
+        should(node.delayTime.maxValue, 'node1.delayTime.maxValue')
+            .beEqualTo(options.maxDelayTime);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-delaynode-interface/idl-test.html b/webaudio/the-audio-api/the-delaynode-interface/idl-test.html
deleted file mode 100644
index eb42a4a..0000000
--- a/webaudio/the-audio-api/the-delaynode-interface/idl-test.html
+++ /dev/null
@@ -1,159 +0,0 @@
-<!DOCTYPE html>
-<html class="a">
-<head>
-<title>DelayNode IDL Test</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/idlharness.js"></script>
-<script src="/resources/WebIDLParser.js"></script>
-<script src="/webaudio/js/helpers.js"></script>
-<style type="text/css">
-    #event-target-idl,
-    #base-audio-context-idl,
-    #audio-node-idl,
-    #audio-param-idl
-    { visibility:hidden; height: 0px;}
-  </style>
-</head>
-<body class="a">
-
-   <pre id="event-target-idl">interface EventTarget {
-  void addEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
-  void removeEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
-  boolean dispatchEvent(Event event);
-};
-
-/*
-callback interface EventListener {
-  void handleEvent(Event event);
-};
-*/
-// Callback interfaces are not supported yet, but that's ok
-interface EventListener {};
-</pre>
-
-   <pre id="base-audio-context-idl">callback DecodeErrorCallback = void (DOMException error);
-
-callback DecodeSuccessCallback = void (AudioBuffer decodedData);
-
-interface BaseAudioContext : EventTarget {
-    readonly        attribute AudioDestinationNode destination;
-    readonly        attribute float                sampleRate;
-    readonly        attribute double               currentTime;
-    readonly        attribute AudioListener        listener;
-    readonly        attribute AudioContextState    state;
-    readonly        attribute double               baseLatency;
-    Promise<void>          resume ();
-                    attribute EventHandler         onstatechange;
-    AudioBuffer            createBuffer (unsigned long numberOfChannels, unsigned long length, float sampleRate);
-    Promise<AudioBuffer>   decodeAudioData (ArrayBuffer audioData, optional DecodeSuccessCallback successCallback, optional DecodeErrorCallback errorCallback);
-    AudioBufferSourceNode  createBufferSource ();
-    ConstantSourceNode     createConstantSource ();
-    ScriptProcessorNode    createScriptProcessor (optional unsigned long bufferSize = 0
-              , optional unsigned long numberOfInputChannels = 2
-              , optional unsigned long numberOfOutputChannels = 2
-              );
-    AnalyserNode           createAnalyser ();
-    GainNode               createGain ();
-    DelayNode              createDelay (optional double maxDelayTime);
-    BiquadFilterNode       createBiquadFilter ();
-    IIRFilterNode          createIIRFilter (sequence<double> feedforward, sequence<double> feedback);
-    WaveShaperNode         createWaveShaper ();
-    PannerNode             createPanner ();
-    StereoPannerNode       createStereoPanner ();
-    ConvolverNode          createConvolver ();
-    ChannelSplitterNode    createChannelSplitter (optional unsigned long numberOfOutputs = 6
-              );
-    ChannelMergerNode      createChannelMerger (optional unsigned long numberOfInputs = 6
-              );
-    DynamicsCompressorNode createDynamicsCompressor ();
-    OscillatorNode         createOscillator ();
-    PeriodicWave           createPeriodicWave (Float32Array real, Float32Array imag, optional PeriodicWaveConstraints constraints);
-};</pre>
-
-   <pre id="audio-node-idl">enum ChannelCountMode {
-    "max",
-    "clamped-max",
-    "explicit"
-};
-
-enum ChannelInterpretation {
-    "speakers",
-    "discrete"
-};
-
-interface AudioNode : EventTarget {
-
-    void connect(AudioNode destination, optional unsigned long output = 0, optional unsigned long input = 0);
-    void connect(AudioParam destination, optional unsigned long output = 0);
-    void disconnect(optional unsigned long output = 0);
-
-    readonly attribute BaseAudioContext context;
-    readonly attribute unsigned long numberOfInputs;
-    readonly attribute unsigned long numberOfOutputs;
-
-    // Channel up-mixing and down-mixing rules for all inputs.
-    attribute unsigned long channelCount;
-    attribute ChannelCountMode channelCountMode;
-    attribute ChannelInterpretation channelInterpretation;
-
-};</pre>
-
-   <pre id="audio-param-idl">interface AudioParam {
-
-                    attribute float value;
-    readonly        attribute float defaultValue;
-    readonly        attribute float minValue;
-    readonly        attribute float maxValue;
-
-    // Parameter automation.
-    void setValueAtTime(float value, double startTime);
-    void linearRampToValueAtTime(float value, double endTime);
-    void exponentialRampToValueAtTime(float value, double endTime);
-
-    // Exponentially approach the target value with a rate having the given time constant.
-    void setTargetAtTime(float target, double startTime, double timeConstant);
-
-    // Sets an array of arbitrary parameter values starting at time for the given duration.
-    // The number of values will be scaled to fit into the desired duration.
-    void setValueCurveAtTime(Float32Array values, double startTime, double duration);
-
-    // Cancels all scheduled parameter changes with times greater than or equal to startTime.
-    void cancelScheduledValues(double startTime);
-
-};</pre>
-
-<pre id="delay-node-idl">dictionary DelayOptions : AudioNodeOptions {
-             double maxDelayTime = 1;
-             double delayTime = 0;
-};
-
-[Constructor(BaseAudioContext context, optional DelayOptions options)]
-interface DelayNode : AudioNode {
-
-    readonly attribute AudioParam delayTime;
-
-};</pre>
-
-  <div id="log"></div>
-
-  <script>
-promise_test(async function() {
-  const webAudioApi = await fetch('/interfaces/web-audio-api.idl').then(r => r.text());
-
-  var idl_array = new IdlArray();
-  idl_array.add_untested_idls(webAudioApi, { only: ['AudioNodeOptions']});
-  idl_array.add_untested_idls(document.getElementById("event-target-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("base-audio-context-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("audio-node-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("audio-param-idl").textContent);
-  idl_array.add_idls(document.getElementById("delay-node-idl").textContent);
-
-  delay_node = (new AudioContext).createDelay();
-
-  idl_array.add_objects({DelayNode: ["delay_node"]});
-  idl_array.test();
-}, 'webaudio Delay interfaces');
-  </script>
-</body>
-</html>
diff --git a/webaudio/the-audio-api/the-dynamicscompressornode-interface/ctor-dynamicscompressor.html b/webaudio/the-audio-api/the-dynamicscompressornode-interface/ctor-dynamicscompressor.html
new file mode 100644
index 0000000..799c187
--- /dev/null
+++ b/webaudio/the-audio-api/the-dynamicscompressornode-interface/ctor-dynamicscompressor.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: DynamicsCompressor
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'DynamicsCompressorNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node =
+            testDefaultConstructor(should, 'DynamicsCompressorNode', context, {
+              prefix: prefix,
+              numberOfInputs: 1,
+              numberOfOutputs: 1,
+              channelCount: 2,
+              channelCountMode: 'clamped-max',
+              channelInterpretation: 'speakers'
+            });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'threshold', value: -24}, {name: 'knee', value: 30},
+          {name: 'ratio', value: 12}, {name: 'reduction', value: 0},
+          {name: 'attack', value: Math.fround(0.003)},
+          {name: 'release', value: 0.25}
+        ]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        // Can't use testAudioNodeOptions because the constraints for this node
+        // are not supported there.
+
+        // Array of test options to be run.  Each entry is a dictionary where
+        // |testAttribute| is the name of the attribute to be tested,
+        // |testValue| is the value to be used, and |expectedErrorType| is the
+        // error type if the test is expected to throw an error.
+        // |expectedErrorType| should be set only if the test does throw.
+        let testOptions = [
+          // Test channel count
+          {
+            testAttribute: 'channelCount',
+            testValue: 1,
+          },
+          {
+            testAttribute: 'channelCount',
+            testValue: 2,
+          },
+          {
+            testAttribute: 'channelCount',
+            testValue: 0,
+            expectedErrorType: 'NotSupportedError'
+          },
+          {
+            testAttribute: 'channelCount',
+            testValue: 3,
+            expectedErrorType: 'NotSupportedError'
+          },
+          {
+            testAttribute: 'channelCount',
+            testValue: 99,
+            expectedErrorType: 'NotSupportedError'
+          },
+          // Test channel count mode
+          {
+            testAttribute: 'channelCountMode',
+            testValue: 'clamped-max',
+          },
+          {
+            testAttribute: 'channelCountMode',
+            testValue: 'explicit',
+          },
+          {
+            testAttribute: 'channelCountMode',
+            testValue: 'max',
+            expectedErrorType: 'NotSupportedError'
+          },
+          {
+            testAttribute: 'channelCountMode',
+            testValue: 'foobar',
+            expectedErrorType: 'TypeError'
+          },
+          // Test channel interpretation
+          {
+            testAttribute: 'channelInterpretation',
+            testValue: 'speakers',
+          },
+          {
+            testAttribute: 'channelInterpretation',
+            testValue: 'discrete',
+          },
+          {
+            testAttribute: 'channelInterpretation',
+            testValue: 'foobar',
+            expectedErrorType: 'TypeError'
+          }
+        ];
+
+        testOptions.forEach((option) => {
+          let nodeOptions = {};
+          nodeOptions[option.testAttribute] = option.testValue;
+
+          testNode(should, context, {
+            nodeOptions: nodeOptions,
+            testAttribute: option.testAttribute,
+            expectedValue: option.testValue,
+            expectedErrorType: option.expectedErrorType
+          });
+        });
+
+        task.done();
+      });
+
+      audit.define('constructor with options', (task, should) => {
+        let node;
+        let options =
+            {threshold: -33, knee: 15, ratio: 7, attack: 0.625, release: 0.125};
+
+        should(
+            () => {
+              node = new DynamicsCompressorNode(context, options);
+            },
+            'node1 = new DynamicsCompressorNode(c, ' + JSON.stringify(options) +
+                ')')
+            .notThrow();
+        should(
+            node instanceof DynamicsCompressorNode,
+            'node1 instanceof DynamicsCompressorNode')
+            .beEqualTo(true);
+
+        should(node.threshold.value, 'node1.threshold.value')
+            .beEqualTo(options.threshold);
+        should(node.knee.value, 'node1.knee.value').beEqualTo(options.knee);
+        should(node.ratio.value, 'node1.ratio.value').beEqualTo(options.ratio);
+        should(node.attack.value, 'node1.attack.value')
+            .beEqualTo(options.attack);
+        should(node.release.value, 'node1.release.value')
+            .beEqualTo(options.release);
+
+        should(node.channelCount, 'node1.channelCount').beEqualTo(2);
+        should(node.channelCountMode, 'node1.channelCountMode')
+            .beEqualTo('clamped-max');
+        should(node.channelInterpretation, 'node1.channelInterpretation')
+            .beEqualTo('speakers');
+
+        task.done();
+      });
+
+      audit.run();
+
+      // Test possible options for DynamicsCompressor constructor.
+      function testNode(should, context, options) {
+        // Node to be tested
+        let node;
+
+        let createNodeFunction = () => {
+          return () => node =
+                     new DynamicsCompressorNode(context, options.nodeOptions);
+        };
+
+        let message = 'new DynamicsCompressorNode(c, ' +
+            JSON.stringify(options.nodeOptions) + ')';
+
+        if (options.expectedErrorType) {
+          should(createNodeFunction(), message)
+              .throw(options.expectedErrorType);
+        } else {
+          should(createNodeFunction(), message).notThrow();
+          should(node[options.testAttribute], 'node.' + options.testAttribute)
+              .beEqualTo(options.expectedValue);
+        }
+      }
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-gainnode-interface/ctor-gain.html b/webaudio/the-audio-api/the-gainnode-interface/ctor-gain.html
new file mode 100644
index 0000000..dec273e
--- /dev/null
+++ b/webaudio/the-audio-api/the-gainnode-interface/ctor-gain.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: Gain
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'GainNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'GainNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [{name: 'gain', value: 1}]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'GainNode');
+        task.done();
+      });
+
+      audit.define('constructor with options', (task, should) => {
+        let node;
+        let options = {
+          gain: -2,
+        };
+
+        should(
+            () => {
+              node = new GainNode(context, options);
+            },
+            'node1 = new GainNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node instanceof GainNode, 'node1 instanceof GainNode')
+            .beEqualTo(true);
+
+        should(node.gain.value, 'node1.gain.value').beEqualTo(options.gain);
+
+        should(node.channelCount, 'node1.channelCount').beEqualTo(2);
+        should(node.channelCountMode, 'node1.channelCountMode')
+            .beEqualTo('max');
+        should(node.channelInterpretation, 'node1.channelInterpretation')
+            .beEqualTo('speakers');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-gainnode-interface/idl-test.html b/webaudio/the-audio-api/the-gainnode-interface/idl-test.html
deleted file mode 100644
index 69606c9..0000000
--- a/webaudio/the-audio-api/the-gainnode-interface/idl-test.html
+++ /dev/null
@@ -1,157 +0,0 @@
-<!DOCTYPE html>
-<html class="a">
-<head>
-<title>GainNode IDL Test</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/idlharness.js"></script>
-<script src="/resources/WebIDLParser.js"></script>
-<script src="/webaudio/js/helpers.js"></script>
-<style type="text/css">
-    #event-target-idl,
-    #base-audio-context-idl,
-    #audio-node-idl,
-    #audio-param-idl
-    { visibility:hidden; height: 0px;}
-  </style>
-</head>
-<body class="a">
-
-   <pre id="event-target-idl">interface EventTarget {
-  void addEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
-  void removeEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
-  boolean dispatchEvent(Event event);
-};
-
-/*
-callback interface EventListener {
-  void handleEvent(Event event);
-};
-*/
-// Callback interfaces are not supported yet, but that's ok
-interface EventListener {};
-</pre>
-
-   <pre id="base-audio-context-idl">callback DecodeErrorCallback = void (DOMException error);
-callback DecodeSuccessCallback = void (AudioBuffer decodedData);
-
-interface BaseAudioContext : EventTarget {
-    readonly        attribute AudioDestinationNode destination;
-    readonly        attribute float                sampleRate;
-    readonly        attribute double               currentTime;
-    readonly        attribute AudioListener        listener;
-    readonly        attribute AudioContextState    state;
-    readonly        attribute double               baseLatency;
-    Promise<void>          resume ();
-                    attribute EventHandler         onstatechange;
-    AudioBuffer            createBuffer (unsigned long numberOfChannels, unsigned long length, float sampleRate);
-    Promise<AudioBuffer>   decodeAudioData (ArrayBuffer audioData, optional DecodeSuccessCallback successCallback, optional DecodeErrorCallback errorCallback);
-    AudioBufferSourceNode  createBufferSource ();
-    ConstantSourceNode     createConstantSource ();
-    ScriptProcessorNode    createScriptProcessor (optional unsigned long bufferSize = 0
-              , optional unsigned long numberOfInputChannels = 2
-              , optional unsigned long numberOfOutputChannels = 2
-              );
-    AnalyserNode           createAnalyser ();
-    GainNode               createGain ();
-    DelayNode              createDelay (optional double maxDelayTime);
-    BiquadFilterNode       createBiquadFilter ();
-    IIRFilterNode          createIIRFilter (sequence<double> feedforward, sequence<double> feedback);
-    WaveShaperNode         createWaveShaper ();
-    PannerNode             createPanner ();
-    StereoPannerNode       createStereoPanner ();
-    ConvolverNode          createConvolver ();
-    ChannelSplitterNode    createChannelSplitter (optional unsigned long numberOfOutputs = 6
-              );
-    ChannelMergerNode      createChannelMerger (optional unsigned long numberOfInputs = 6
-              );
-    DynamicsCompressorNode createDynamicsCompressor ();
-    OscillatorNode         createOscillator ();
-    PeriodicWave           createPeriodicWave (Float32Array real, Float32Array imag, optional PeriodicWaveConstraints constraints);
-};</pre>
-
-   <pre id="audio-node-idl">enum ChannelCountMode {
-    "max",
-    "clamped-max",
-    "explicit"
-};
-
-enum ChannelInterpretation {
-    "speakers",
-    "discrete"
-};
-
-interface AudioNode : EventTarget {
-
-    void connect(AudioNode destination, optional unsigned long output = 0, optional unsigned long input = 0);
-    void connect(AudioParam destination, optional unsigned long output = 0);
-    void disconnect(optional unsigned long output = 0);
-
-    readonly attribute BaseAudioContext context;
-    readonly attribute unsigned long numberOfInputs;
-    readonly attribute unsigned long numberOfOutputs;
-
-    // Channel up-mixing and down-mixing rules for all inputs.
-    attribute unsigned long channelCount;
-    attribute ChannelCountMode channelCountMode;
-    attribute ChannelInterpretation channelInterpretation;
-
-};</pre>
-
-   <pre id="audio-param-idl">interface AudioParam {
-
-                    attribute float value;
-    readonly        attribute float defaultValue;
-    readonly        attribute float minValue;
-    readonly        attribute float maxValue;
-
-    // Parameter automation.
-    void setValueAtTime(float value, double startTime);
-    void linearRampToValueAtTime(float value, double endTime);
-    void exponentialRampToValueAtTime(float value, double endTime);
-
-    // Exponentially approach the target value with a rate having the given time constant.
-    void setTargetAtTime(float target, double startTime, double timeConstant);
-
-    // Sets an array of arbitrary parameter values starting at time for the given duration.
-    // The number of values will be scaled to fit into the desired duration.
-    void setValueCurveAtTime(Float32Array values, double startTime, double duration);
-
-    // Cancels all scheduled parameter changes with times greater than or equal to startTime.
-    void cancelScheduledValues(double startTime);
-
-};</pre>
-
-<pre id="gain-node-idl">dictionary GainOptions : AudioNodeOptions {
-             float gain = 1.0;
-};
-
-[Constructor(BaseAudioContext context, optional GainOptions options)]
-interface GainNode : AudioNode {
-
-    readonly attribute AudioParam gain;
-
-};</pre>
-
-  <div id="log"></div>
-
-  <script>
-promise_test(async function () {
-  const webAudioApi = await fetch('/interfaces/web-audio-api.idl').then(r => r.text());
-
-  var idl_array = new IdlArray();
-  idl_array.add_untested_idls(webAudioApi, { only: ['AudioNodeOptions'] });
-  idl_array.add_untested_idls(document.getElementById("event-target-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("base-audio-context-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("audio-node-idl").textContent);
-  idl_array.add_untested_idls(document.getElementById("audio-param-idl").textContent);
-  idl_array.add_idls(document.getElementById("gain-node-idl").textContent);
-
-  gain_node = (new AudioContext).createGain();
-
-  idl_array.add_objects({GainNode: ["gain_node"]});
-  idl_array.test();
-}, 'webaudio Gain interfaces');
-  </script>
-</body>
-</html>
diff --git a/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html b/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html
new file mode 100644
index 0000000..bb89512
--- /dev/null
+++ b/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: IIRFilter
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'IIRFilterNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'IIRFilterNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers',
+          constructorOptions: {feedforward: [1], feedback: [1, -.9]}
+        });
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(
+            should, context, 'IIRFilterNode',
+            {additionalOptions: {feedforward: [1, 1], feedback: [1, .5]}});
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+
+        let options = {feedback: [1, .5]};
+        should(
+            () => {
+              node = new IIRFilterNode(context, options);
+            },
+            'node = new IIRFilterNode(, ' + JSON.stringify(options) + ')')
+            .throw('TypeError');
+
+        options = {feedforward: [1, 0.5]};
+        should(
+            () => {
+              node = new IIRFilterNode(context, options);
+            },
+            'node = new IIRFilterNode(c, ' + JSON.stringify(options) + ')')
+            .throw('TypeError');
+
+        task.done();
+      });
+
+      // Test functionality of constructor.  This is needed because we have no
+      // way of determining if the filter coefficients were were actually set
+      // appropriately.
+
+      // TODO(rtoy): This functionality test should be moved out to a separate
+      // file.
+      audit.define('functionality', (task, should) => {
+        let options = {feedback: [1, .5], feedforward: [1, 1]};
+
+        // Create two-channel offline context; sample rate and length are fairly
+        // arbitrary.  Channel 0 contains the test output and channel 1 contains
+        // the expected output.
+        let sampleRate = 48000;
+        let renderLength = 0.125;
+        let testContext =
+            new OfflineAudioContext(2, renderLength * sampleRate, sampleRate);
+
+        // The test node uses the constructor.  The reference node creates the
+        // same filter but uses the old factory method.
+        let testNode = new IIRFilterNode(testContext, options);
+        let refNode = testContext.createIIRFilter(
+            Float32Array.from(options.feedforward),
+            Float32Array.from(options.feedback));
+
+        let source = testContext.createOscillator();
+        source.connect(testNode);
+        source.connect(refNode);
+
+        let merger = testContext.createChannelMerger(
+            testContext.destination.channelCount);
+
+        testNode.connect(merger, 0, 0);
+        refNode.connect(merger, 0, 1);
+
+        merger.connect(testContext.destination);
+
+        source.start();
+        testContext.startRendering()
+            .then(function(resultBuffer) {
+              let actual = resultBuffer.getChannelData(0);
+              let expected = resultBuffer.getChannelData(1);
+
+              // The output from the two channels should be exactly equal
+              // because exactly the same IIR filter should have been created.
+              should(actual, 'Output of filter using new IIRFilter(...)')
+                  .beEqualToArray(expected);
+            })
+            .then(() => task.done());
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-offlineaudiocontext-interface/ctor-offlineaudiocontext.html b/webaudio/the-audio-api/the-offlineaudiocontext-interface/ctor-offlineaudiocontext.html
new file mode 100644
index 0000000..79aafe7
--- /dev/null
+++ b/webaudio/the-audio-api/the-offlineaudiocontext-interface/ctor-offlineaudiocontext.html
@@ -0,0 +1,203 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test Constructor: OfflineAudioContext</title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+
+  <body>
+    <script>
+      let audit = Audit.createTaskRunner();
+
+      // Just a simple test of the 3-arg constructor; This should be
+      // well-covered by other layout tests that use the 3-arg constructor.
+      audit.define(
+          {label: 'basic', description: 'Old-style constructor'},
+          (task, should) => {
+            let context;
+
+            // First and only arg should be a dictionary.
+            should(() => {
+              new OfflineAudioContext(3);
+            }, 'new OfflineAudioContext(3)').throw('TypeError');
+
+            // Constructor needs 1 or 3 args, so 2 should throw.
+            should(() => {
+              new OfflineAudioContext(3, 42);
+            }, 'new OfflineAudioContext(3, 42)').throw('TypeError');
+
+            // Valid constructor
+            should(() => {
+              context = new OfflineAudioContext(3, 42, 12345);
+            }, 'context = new OfflineAudioContext(3, 42, 12345)').notThrow();
+
+            // Verify that the context was constructed correctly.
+            should(context.length, 'context.length').beEqualTo(42);
+            should(context.sampleRate, 'context.sampleRate').beEqualTo(12345);
+            should(
+                context.destination.channelCount,
+                'context.destination.channelCount')
+                .beEqualTo(3);
+            should(
+                context.destination.channelCountMode,
+                'context.destination.channelCountMode')
+                .beEqualTo('explicit');
+            should(
+                context.destination.channelInterpretation,
+                'context.destination.channelInterpretation')
+                .beEqualTo('speakers');
+            task.done();
+          });
+
+      // Test constructor throws an error if the required members of the
+      // dictionary are not given.
+      audit.define(
+          {label: 'options-1', description: 'Required options'},
+          (task, should) => {
+            let context2;
+
+            // No args should throw
+            should(() => {
+              new OfflineAudioContext();
+            }, 'new OfflineAudioContext()').throw('TypeError');
+
+            // Empty OfflineAudioContextOptions should throw
+            should(() => {
+              new OfflineAudioContext({});
+            }, 'new OfflineAudioContext({})').throw('TypeError');
+
+            let options = {length: 42};
+            // sampleRate is required.
+            should(
+                () => {
+                  new OfflineAudioContext(options);
+                },
+                'new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .throw('TypeError');
+
+            options = {sampleRate: 12345};
+            // length is required.
+            should(
+                () => {
+                  new OfflineAudioContext(options);
+                },
+                'new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .throw('TypeError');
+
+            // Valid constructor.  Verify that the resulting context has the
+            // correct values.
+            options = {length: 42, sampleRate: 12345};
+            should(
+                () => {
+                  context2 = new OfflineAudioContext(options);
+                },
+                'c2 = new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .notThrow();
+            should(
+                context2.destination.channelCount,
+                'c2.destination.channelCount')
+                .beEqualTo(1);
+            should(context2.length, 'c2.length').beEqualTo(options.length);
+            should(context2.sampleRate, 'c2.sampleRate')
+                .beEqualTo(options.sampleRate);
+            should(
+                context2.destination.channelCountMode,
+                'c2.destination.channelCountMode')
+                .beEqualTo('explicit');
+            should(
+                context2.destination.channelInterpretation,
+                'c2.destination.channelInterpretation')
+                .beEqualTo('speakers');
+
+            task.done();
+          });
+
+      // Constructor should throw errors for invalid values specified by
+      // OfflineAudioContextOptions.
+      audit.define(
+          {label: 'options-2', description: 'Invalid options'},
+          (task, should) => {
+            let options = {length: 42, sampleRate: 8000, numberOfChannels: 33};
+
+            // channelCount too large.
+            should(
+                () => {
+                  new OfflineAudioContext(options);
+                },
+                'new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .throw('NotSupportedError');
+
+            // length cannot be 0
+            options = {length: 0, sampleRate: 8000};
+            should(
+                () => {
+                  new OfflineAudioContext(options);
+                },
+                'new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .throw('NotSupportedError');
+
+            // sampleRate outside valid range
+            options = {length: 1, sampleRate: 1};
+            should(
+                () => {
+                  new OfflineAudioContext(options);
+                },
+                'new OfflineAudioContext(' + JSON.stringify(options) + ')')
+                .throw('NotSupportedError');
+
+            task.done();
+          });
+
+      audit.define(
+          {label: 'options-3', description: 'Valid options'},
+          (task, should) => {
+            let context;
+            let options = {
+              length: 1,
+              sampleRate: 8000,
+            };
+
+            // Verify context with valid constructor has the correct values.
+            should(
+                () => {
+                  context = new OfflineAudioContext(options);
+                },
+                'c = new OfflineAudioContext' + JSON.stringify(options) + ')')
+                .notThrow();
+            should(context.length, 'c.length').beEqualTo(options.length);
+            should(context.sampleRate, 'c.sampleRate')
+                .beEqualTo(options.sampleRate);
+            should(
+                context.destination.channelCount, 'c.destination.channelCount')
+                .beEqualTo(1);
+            should(
+                context.destination.channelCountMode,
+                'c.destination.channelCountMode')
+                .beEqualTo('explicit');
+            should(
+                context.destination.channelInterpretation,
+                'c.destination.channelCountMode')
+                .beEqualTo('speakers');
+
+            options.numberOfChannels = 7;
+            should(
+                () => {
+                  context = new OfflineAudioContext(options);
+                },
+                'c = new OfflineAudioContext' + JSON.stringify(options) + ')')
+                .notThrow();
+            should(
+                context.destination.channelCount, 'c.destination.channelCount')
+                .beEqualTo(options.numberOfChannels);
+
+            task.done();
+          });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html b/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html
new file mode 100644
index 0000000..aaf77ae
--- /dev/null
+++ b/webaudio/the-audio-api/the-oscillatornode-interface/ctor-oscillator.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: Oscillator
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'OscillatorNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'OscillatorNode', context, {
+          prefix: prefix,
+          numberOfInputs: 0,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(
+            should, node, prefix,
+            [{name: 'type', value: 'sine'}, {name: 'frequency', value: 440}]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'OscillatorNode');
+        task.done();
+      });
+
+      audit.define('constructor options', (task, should) => {
+        let node;
+        let options = {type: 'sawtooth', detune: 7, frequency: 918};
+
+        should(
+            () => {
+              node = new OscillatorNode(context, options);
+            },
+            'node1 = new OscillatorNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+
+        should(node.type, 'node1.type').beEqualTo(options.type);
+        should(node.detune.value, 'node1.detune.value')
+            .beEqualTo(options.detune);
+        should(node.frequency.value, 'node1.frequency.value')
+            .beEqualTo(options.frequency);
+
+        should(node.channelCount, 'node1.channelCount').beEqualTo(2);
+        should(node.channelCountMode, 'node1.channelCountMode')
+            .beEqualTo('max');
+        should(node.channelInterpretation, 'node1.channelInterpretation')
+            .beEqualTo('speakers');
+
+        // Test that type and periodicWave options work as described.
+        options = {
+          type: 'sine',
+          periodicWave: new PeriodicWave(context, {real: [1, 1]})
+        };
+        should(() => {
+          node = new OscillatorNode(context, options);
+        }, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow();
+
+        options = {type: 'custom'};
+        should(
+            () => {
+              node = new OscillatorNode(context, options);
+            },
+            'new OscillatorNode(c, ' + JSON.stringify(options) + ')')
+            .throw('InvalidStateError');
+
+        options = {
+          type: 'custom',
+          periodicWave: new PeriodicWave(context, {real: [1, 1]})
+        };
+        should(() => {
+          node = new OscillatorNode(context, options);
+        }, 'new OscillatorNode(, ' + JSON.stringify(options) + ')').notThrow();
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html b/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html
new file mode 100644
index 0000000..48b368d
--- /dev/null
+++ b/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: Panner
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'PannerNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'PannerNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'clamped-max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'panningModel', value: 'equalpower'},
+          {name: 'positionX', value: 0}, {name: 'positionY', value: 0},
+          {name: 'positionZ', value: 0}, {name: 'orientationX', value: 1},
+          {name: 'orientationY', value: 0}, {name: 'orientationZ', value: 0},
+          {name: 'distanceModel', value: 'inverse'},
+          {name: 'refDistance', value: 1}, {name: 'maxDistance', value: 10000},
+          {name: 'rolloffFactor', value: 1},
+          {name: 'coneInnerAngle', value: 360},
+          {name: 'coneOuterAngle', value: 360},
+          {name: 'coneOuterGain', value: 0}
+        ]);
+
+        // Test the listener too, while we're at it.
+        let listenerAttributes = [
+          {name: 'positionX', value: 0},
+          {name: 'positionY', value: 0},
+          {name: 'positionZ', value: 0},
+          {name: 'forwardX', value: 0},
+          {name: 'forwardY', value: 0},
+          {name: 'forwardZ', value: -1},
+          {name: 'upX', value: 0},
+          {name: 'upY', value: 1},
+          {name: 'upZ', value: 0},
+        ];
+
+        listenerAttributes.forEach((item) => {
+          should(
+              context.listener[item.name].value,
+              'context.listener.' + item.name + '.value')
+              .beEqualTo(item.value);
+        });
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        // Can't use testAudioNodeOptions because the constraints for this node
+        // are not supported there.
+        let node;
+        let success = true;
+
+        // Test that we can set the channel count to 1 or 2.
+        let options = {channelCount: 1};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node1 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelCount, 'node1.channelCount')
+            .beEqualTo(options.channelCount);
+
+        options = {channelCount: 2};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node2 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelCount, 'node2.channelCount')
+            .beEqualTo(options.channelCount);
+
+        // Test that other channel counts throw an error
+        options = {channelCount: 0};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {channelCount: 3};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {channelCount: 99};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        // Test channelCountMode.  A mode of "max" is illegal, but others are
+        // ok.
+        options = {channelCountMode: 'clamped-max'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node3 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelCountMode, 'node3.channelCountMode')
+            .beEqualTo(options.channelCountMode);
+
+        options = {channelCountMode: 'explicit'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node4 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelCountMode, 'node4.channelCountMode')
+            .beEqualTo(options.channelCountMode);
+
+        options = {channelCountMode: 'max'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('NotSupportedError');
+
+        options = {channelCountMode: 'foobar'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, " + JSON.stringify(options) + ")')
+            .throw('TypeError');
+
+        // Test channelInterpretation.
+        options = {channelInterpretation: 'speakers'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node5 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelInterpretation, 'node5.channelInterpretation')
+            .beEqualTo(options.channelInterpretation);
+
+        options = {channelInterpretation: 'discrete'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node6 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node.channelInterpretation, 'node6.channelInterpretation')
+            .beEqualTo(options.channelInterpretation);
+
+        options = {channelInterpretation: 'foobar'};
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .throw('TypeError');
+
+        task.done();
+      });
+
+      audit.define('constructor with options', (task, should) => {
+        let node;
+        let success = true;
+        let options = {
+          panningModel: 'HRTF',
+          // We use full double float values here to verify also that the actual
+          // AudioParam value is properly rounded to a float.  The actual value
+          // is immaterial as long as x != Math.fround(x).
+          positionX: Math.SQRT2,
+          positionY: 2 * Math.SQRT2,
+          positionZ: 3 * Math.SQRT2,
+          orientationX: -Math.SQRT2,
+          orientationY: -2 * Math.SQRT2,
+          orientationZ: -3 * Math.SQRT2,
+          distanceModel: 'linear',
+          // We use full double float values here to verify also that the actual
+          // attribute is a double float.  The actual value is immaterial as
+          // long as x != Math.fround(x).
+          refDistance: Math.PI,
+          maxDistance: 2 * Math.PI,
+          rolloffFactor: 3 * Math.PI,
+          coneInnerAngle: 4 * Math.PI,
+          coneOuterAngle: 5 * Math.PI,
+          coneOuterGain: 6 * Math.PI
+        };
+
+        should(
+            () => {
+              node = new PannerNode(context, options);
+            },
+            'node = new PannerNode(c, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(node instanceof PannerNode, 'node instanceof PannerNode')
+            .beEqualTo(true);
+
+        should(node.panningModel, 'node.panningModel')
+            .beEqualTo(options.panningModel);
+        should(node.positionX.value, 'node.positionX.value')
+            .beEqualTo(Math.fround(options.positionX));
+        should(node.positionY.value, 'node.positionY.value')
+            .beEqualTo(Math.fround(options.positionY));
+        should(node.positionZ.value, 'node.positionZ.value')
+            .beEqualTo(Math.fround(options.positionZ));
+        should(node.orientationX.value, 'node.orientationX.value')
+            .beEqualTo(Math.fround(options.orientationX));
+        should(node.orientationY.value, 'node.orientationY.value')
+            .beEqualTo(Math.fround(options.orientationY));
+        should(node.orientationZ.value, 'node.orientationZ.value')
+            .beEqualTo(Math.fround(options.orientationZ));
+        should(node.distanceModel, 'node.distanceModel')
+            .beEqualTo(options.distanceModel);
+        should(node.refDistance, 'node.refDistance')
+            .beEqualTo(options.refDistance);
+        should(node.maxDistance, 'node.maxDistance')
+            .beEqualTo(options.maxDistance);
+        should(node.rolloffFactor, 'node.rolloffFactor')
+            .beEqualTo(options.rolloffFactor);
+        should(node.coneInnerAngle, 'node.coneInnerAngle')
+            .beEqualTo(options.coneInnerAngle);
+        should(node.coneOuterAngle, 'node.coneOuterAngle')
+            .beEqualTo(options.coneOuterAngle);
+        should(node.coneOuterGain, 'node.coneOuterGain')
+            .beEqualTo(options.coneOuterGain);
+
+        should(node.channelCount, 'node.channelCount').beEqualTo(2);
+        should(node.channelCountMode, 'node.channelCountMode')
+            .beEqualTo('clamped-max');
+        should(node.channelInterpretation, 'node.channelInterpretation')
+            .beEqualTo('speakers');
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-stereopanner-interface/ctor-stereopanner.html b/webaudio/the-audio-api/the-stereopanner-interface/ctor-stereopanner.html
new file mode 100644
index 0000000..9de58cf
--- /dev/null
+++ b/webaudio/the-audio-api/the-stereopanner-interface/ctor-stereopanner.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: StereoPanner
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('invalid constructor', (task, should) => {
+        testInvalidConstructor(should, 'StereoPannerNode', context);
+        task.done();
+      });
+
+      audit.define('default constructor', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'StereoPannerNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'clamped-max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [{name: 'pan', value: 0}]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        // Can't use testAudioNodeOptions because the constraints for this node
+        // are not supported there.
+        let node;
+
+        // An array of tests.
+        [{
+          // Test that we can set the channel count to 1 or 2 and that other
+          // channel counts throw an error.
+          attribute: 'channelCount',
+          tests: [
+            {value: 1}, {value: 2}, {value: 0, error: 'NotSupportedError'},
+            {value: 3, error: 'NotSupportedError'},
+            {value: 99, error: 'NotSupportedError'}
+          ]
+        },
+         {
+           // Test channelCountMode.  A mode of "max" is illegal, but others are
+           // ok.  But also throw an error of unknown values.
+           attribute: 'channelCountMode',
+           tests: [
+             {value: 'clamped-max'}, {value: 'explicit'},
+             {value: 'max', error: 'NotSupportedError'},
+             {value: 'foobar', error: 'TypeError'}
+           ]
+         },
+         {
+           // Test channelInterpretation can be set for valid values and an
+           // error is thrown for others.
+           attribute: 'channelInterpretation',
+           tests: [
+             {value: 'speakers'}, {value: 'discrete'},
+             {value: 'foobar', error: 'TypeError'}
+           ]
+         }].forEach(entry => {
+          entry.tests.forEach(testItem => {
+            let options = {};
+            options[entry.attribute] = testItem.value;
+            let method = testItem.error ? 'throw' : 'notThrow';
+
+            should(
+                () => {
+                  node = new StereoPannerNode(context, options);
+                },
+                `new StereoPannerNode(c, ${JSON.stringify(options)})`)[method](
+                testItem.error);
+            if (!testItem.error)
+              should(node[entry.attribute], `node.${entry.attribute}`)
+                  .beEqualTo(options[entry.attribute]);
+          });
+        });
+
+        task.done();
+      });
+
+      audit.define('constructor with options', (task, should) => {
+        let node;
+        let options = {
+          pan: 0.75,
+        };
+
+        should(
+            () => {
+              node = new StereoPannerNode(context, options);
+            },
+            'node1 = new StereoPannerNode(, ' + JSON.stringify(options) + ')')
+            .notThrow();
+        should(
+            node instanceof StereoPannerNode,
+            'node1 instanceof StereoPannerNode')
+            .beEqualTo(true);
+
+        should(node.pan.value, 'node1.pan.value').beEqualTo(options.pan);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webaudio/the-audio-api/the-waveshapernode-interface/ctor-waveshaper.html b/webaudio/the-audio-api/the-waveshapernode-interface/ctor-waveshaper.html
new file mode 100644
index 0000000..7aa33ca
--- /dev/null
+++ b/webaudio/the-audio-api/the-waveshapernode-interface/ctor-waveshaper.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>
+      Test Constructor: WaveShaper
+    </title>
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="/webaudio/resources/audit-util.js"></script>
+    <script src="/webaudio/resources/audit.js"></script>
+    <script src="/webaudio/resources/audionodeoptions.js"></script>
+  </head>
+  <body>
+    <script id="layout-test-code">
+      let context;
+
+      let audit = Audit.createTaskRunner();
+
+      audit.define('initialize', (task, should) => {
+        context = initializeContext(should);
+        task.done();
+      });
+
+      audit.define('incorrect construction', (task, should) => {
+        testInvalidConstructor(should, 'WaveShaperNode', context);
+        task.done();
+      });
+
+      audit.define('valid default construction', (task, should) => {
+        let prefix = 'node0';
+        let node = testDefaultConstructor(should, 'WaveShaperNode', context, {
+          prefix: prefix,
+          numberOfInputs: 1,
+          numberOfOutputs: 1,
+          channelCount: 2,
+          channelCountMode: 'max',
+          channelInterpretation: 'speakers'
+        });
+
+        testDefaultAttributes(should, node, prefix, [
+          {name: 'curve', value: null}, {name: 'oversample', value: 'none'}
+        ]);
+
+        task.done();
+      });
+
+      audit.define('test AudioNodeOptions', (task, should) => {
+        testAudioNodeOptions(should, context, 'WaveShaperNode');
+        task.done();
+      });
+
+      audit.define('valid non-default', (task, should) => {
+        // Construct an WaveShaperNode with options
+        let options = {curve: Float32Array.from([1, 2, 3]), oversample: '4x'};
+        let node;
+
+        let message =
+            'node1 = new WaveShaperNode(, ' + JSON.stringify(options) + ')';
+        should(() => {
+          node = new WaveShaperNode(context, options);
+        }, message).notThrow();
+        should(node.curve, 'node1.curve').beEqualToArray(options.curve);
+        should(node.oversample, 'node1.oversample')
+            .beEqualTo(options.oversample);
+
+        task.done();
+      });
+
+      audit.run();
+    </script>
+  </body>
+</html>
diff --git a/webdriver/tests/close_window/close.py b/webdriver/tests/close_window/close.py
new file mode 100644
index 0000000..4dbc45e
--- /dev/null
+++ b/webdriver/tests/close_window/close.py
@@ -0,0 +1,38 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def close(session):
+    return session.transport.send("DELETE", "session/%s/window" % session.session_id)
+
+
+def test_no_browsing_context(session, create_window):
+    new_handle = create_window()
+
+    session.window_handle = new_handle
+    session.close()
+    assert new_handle not in session.handles
+
+    response = close(session)
+    assert_error(response, "no such window")
+
+
+def test_close_browsing_context(session, create_window):
+    handles = session.handles
+
+    new_handle = create_window()
+    session.window_handle = new_handle
+
+    response = close(session)
+    value = assert_success(response, handles)
+    assert session.handles == handles
+    assert new_handle not in value
+
+
+def test_close_last_browsing_context(session):
+    assert len(session.handles) == 1
+    response = close(session)
+
+    assert_success(response, [])
+
+    # With no more open top-level browsing contexts, the session is closed.
+    session.session_id = None
diff --git a/webdriver/tests/close_window/user_prompts.py b/webdriver/tests/close_window/user_prompts.py
new file mode 100644
index 0000000..069bab7
--- /dev/null
+++ b/webdriver/tests/close_window/user_prompts.py
@@ -0,0 +1,66 @@
+from tests.support.asserts import assert_error, assert_dialog_handled
+from tests.support.fixtures import create_dialog, create_window
+from tests.support.inline import inline
+
+
+def close(session):
+    return session.transport.send("DELETE", "session/%s/window" % session.session_id)
+
+
+def test_handle_prompt_dismiss_and_notify():
+    """TODO"""
+
+
+def test_handle_prompt_accept_and_notify():
+    """TODO"""
+
+
+def test_handle_prompt_ignore():
+    """TODO"""
+
+
+def test_handle_prompt_accept(new_session, add_browser_capabilites):
+    _, session = new_session({"capabilities": {
+        "alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
+    session.window_handle = create_window(session)()
+
+    session.url = inline("<title>WD doc title</title>")
+
+    create_dialog(session)("alert", text="dismiss #1", result_var="dismiss1")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #1")
+
+    create_dialog(session)("confirm", text="dismiss #2", result_var="dismiss2")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #2")
+
+    create_dialog(session)("prompt", text="dismiss #3", result_var="dismiss3")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #3")
+
+
+def test_handle_prompt_missing_value(session, create_dialog, create_window):
+    session.window_handle = create_window()
+
+    session.url = inline("<title>WD doc title</title>")
+    create_dialog("alert", text="dismiss #1", result_var="dismiss1")
+
+    response = close(session)
+
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #1")
+
+    create_dialog("confirm", text="dismiss #2", result_var="dismiss2")
+
+    response = close(session)
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #2")
+
+    create_dialog("prompt", text="dismiss #3", result_var="dismiss3")
+
+    response = close(session)
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #3")
diff --git a/webdriver/tests/cookies/add_cookie.py b/webdriver/tests/cookies/add_cookie.py
index 88b79fb..f865a08 100644
--- a/webdriver/tests/cookies/add_cookie.py
+++ b/webdriver/tests/cookies/add_cookie.py
@@ -1,70 +1,62 @@
-from tests.support.fixtures import clear_all_cookies
 from datetime import datetime, timedelta
 
+from tests.support.asserts import assert_success
+from tests.support.fixtures import clear_all_cookies
+
+
+def add_cookie(session, cookie):
+    return session.transport.send(
+        "POST", "session/{session_id}/cookie".format(**vars(session)),
+        {"cookie": cookie})
+
+
 def test_add_domain_cookie(session, url, server_config):
+    new_cookie = {
+        "name": "hello",
+        "value": "world",
+        "domain": server_config["browser_host"],
+        "path": "/",
+        "httpOnly": False,
+        "secure": False
+    }
+
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "domain": server_config["domains"][""],
-            "path": "/",
-            "httpOnly": False,
-            "secure": False
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = add_cookie(session, new_cookie)
+    assert_success(result)
 
-    cookie = result.body["value"][0]
+    cookie = session.cookies("hello")
+    assert "domain" in cookie
+    assert isinstance(cookie["domain"], basestring)
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
     assert isinstance(cookie["value"], basestring)
-    assert "domain" in cookie
-    assert isinstance(cookie["domain"], basestring)
 
     assert cookie["name"] == "hello"
     assert cookie["value"] == "world"
-    assert cookie["domain"] == ".%s" % server_config["domains"][""] or cookie["domain"] == "%s" % server_config["domains"][""]
+    assert cookie["domain"] == server_config["browser_host"] or \
+        cookie["domain"] == ".%s" % server_config["browser_host"]
+
 
 def test_add_cookie_for_ip(session, url, server_config, configuration):
-    session.url = "http://127.0.0.1:%s/common/blank.html" % (server_config["ports"]["http"][0])
-    clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "domain": "127.0.0.1",
-            "path": "/",
-            "httpOnly": False,
-            "secure": False
-        }
+    new_cookie = {
+        "name": "hello",
+        "value": "world",
+        "domain": "127.0.0.1",
+        "path": "/",
+        "httpOnly": False,
+        "secure": False
     }
 
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
+    session.url = "http://127.0.0.1:%s/common/blank.html" % (server_config["ports"]["http"][0])
+    clear_all_cookies(session)
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = add_cookie(session, new_cookie)
+    assert_success(result)
 
-    cookie = result.body["value"][0]
+    cookie = session.cookies("hello")
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -76,30 +68,24 @@
     assert cookie["value"] == "world"
     assert cookie["domain"] == "127.0.0.1"
 
+
 def test_add_non_session_cookie(session, url):
+    a_year_from_now = int(
+        (datetime.utcnow() + timedelta(days=365) - datetime.utcfromtimestamp(0)).total_seconds())
+
+    new_cookie = {
+        "name": "hello",
+        "value": "world",
+        "expiry": a_year_from_now
+    }
+
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    a_year_from_now = int((datetime.utcnow() + timedelta(days=365) - datetime.utcfromtimestamp(0)).total_seconds())
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "expiry": a_year_from_now
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = add_cookie(session, new_cookie)
+    assert_success(result)
 
-    cookie = result.body["value"][0]
+    cookie = session.cookies("hello")
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -111,28 +97,20 @@
     assert cookie["value"] == "world"
     assert cookie["expiry"] == a_year_from_now
 
+
 def test_add_session_cookie(session, url):
+    new_cookie = {
+        "name": "hello",
+        "value": "world"
+    }
+
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world"
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = add_cookie(session, new_cookie)
+    assert_success(result)
 
-    cookie = result.body["value"][0]
+    cookie = session.cookies("hello")
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -143,29 +121,21 @@
     assert cookie["name"] == "hello"
     assert cookie["value"] == "world"
 
+
 def test_add_session_cookie_with_leading_dot_character_in_domain(session, url, server_config):
+    new_cookie = {
+        "name": "hello",
+        "value": "world",
+        "domain": ".%s" % server_config["browser_host"]
+    }
+
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "domain": ".%s" % server_config["domains"][""]
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = add_cookie(session, new_cookie)
+    assert_success(result)
 
-    cookie = result.body["value"][0]
+    cookie = session.cookies("hello")
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -175,4 +145,5 @@
 
     assert cookie["name"] == "hello"
     assert cookie["value"] == "world"
-    assert cookie["domain"] == ".%s" % server_config["domains"][""] or cookie["domain"] == "%s" % server_config["domains"][""]
+    assert cookie["domain"] == server_config["browser_host"] or \
+        cookie["domain"] == ".%s" % server_config["browser_host"]
diff --git a/webdriver/tests/cookies/get_named_cookie.py b/webdriver/tests/cookies/get_named_cookie.py
index 806dda3..cbf7195 100644
--- a/webdriver/tests/cookies/get_named_cookie.py
+++ b/webdriver/tests/cookies/get_named_cookie.py
@@ -1,19 +1,28 @@
-from tests.support.inline import inline
-from tests.support.fixtures import clear_all_cookies
 from datetime import datetime, timedelta
 
+from tests.support.asserts import assert_success
+from tests.support.fixtures import clear_all_cookies
+from tests.support.inline import inline
+
+
+def get_named_cookie(session, name):
+    return session.transport.send(
+        "GET", "session/{session_id}/cookie/{name}".format(
+            session_id=session.session_id,
+            name=name))
+
+
 def test_get_named_session_cookie(session, url):
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
     session.execute_script("document.cookie = 'foo=bar'")
 
-    result = session.transport.send("GET", "session/%s/cookie/foo" % session.session_id)
-    assert result.status == 200
-    assert isinstance(result.body["value"], dict)
+    result = get_named_cookie(session, "foo")
+    cookie = assert_success(result)
+    assert isinstance(cookie, dict)
 
     # table for cookie conversion
     # https://w3c.github.io/webdriver/webdriver-spec.html#dfn-table-for-cookie-conversion
-    cookie = result.body["value"]
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -32,6 +41,7 @@
     assert cookie["name"] == "foo"
     assert cookie["value"] == "bar"
 
+
 def test_get_named_cookie(session, url):
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
@@ -41,14 +51,10 @@
     a_year_from_now = (datetime.utcnow() + timedelta(days=365)).strftime(utc_string_format)
     session.execute_script("document.cookie = 'foo=bar;expires=%s'" % a_year_from_now)
 
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    result = get_named_cookie(session, "foo")
+    cookie = assert_success(result)
+    assert isinstance(cookie, dict)
 
-    cookie = result.body["value"][0]
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
@@ -59,39 +65,39 @@
     assert cookie["name"] == "foo"
     assert cookie["value"] == "bar"
     # convert from seconds since epoch
-    assert datetime.utcfromtimestamp(cookie["expiry"]).strftime(utc_string_format) == a_year_from_now
+    assert datetime.utcfromtimestamp(
+        cookie["expiry"]).strftime(utc_string_format) == a_year_from_now
+
 
 def test_duplicated_cookie(session, url, server_config):
+    new_cookie = {
+        "name": "hello",
+        "value": "world",
+        "domain": server_config["browser_host"],
+        "path": "/",
+        "http_only": False,
+        "secure": False
+    }
+
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    create_cookie_request = {
-        "cookie": {
-            "name": "hello",
-            "value": "world",
-            "domain": server_config["domains"][""],
-            "path": "/",
-            "httpOnly": False,
-            "secure": False
-        }
-    }
-    result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
-    assert result.status == 200
-    assert "value" in result.body
-    assert result.body["value"] is None
 
-    session.url = inline("<script>document.cookie = 'hello=newworld; domain=%s; path=/';</script>" % server_config["domains"][""])
-    result = session.transport.send("GET", "session/%s/cookie" % session.session_id)
-    assert result.status == 200
-    assert "value" in result.body
-    assert isinstance(result.body["value"], list)
-    assert len(result.body["value"]) == 1
-    assert isinstance(result.body["value"][0], dict)
+    session.set_cookie(**new_cookie)
+    session.url = inline("""
+      <script>
+        document.cookie = '{name}=newworld; domain={domain}; path=/';
+      </script>""".format(
+        name=new_cookie["name"],
+        domain=server_config["browser_host"]))
 
-    cookie = result.body["value"][0]
+    result = get_named_cookie(session, new_cookie["name"])
+    cookie = assert_success(result)
+    assert isinstance(cookie, dict)
+
     assert "name" in cookie
     assert isinstance(cookie["name"], basestring)
     assert "value" in cookie
     assert isinstance(cookie["value"], basestring)
 
-    assert cookie["name"] == "hello"
+    assert cookie["name"] == new_cookie["name"]
     assert cookie["value"] == "newworld"
diff --git a/webdriver/tests/element_click/bubbling.py b/webdriver/tests/element_click/bubbling.py
index 7bf405c..a5377dd 100644
--- a/webdriver/tests/element_click/bubbling.py
+++ b/webdriver/tests/element_click/bubbling.py
@@ -1,6 +1,7 @@
 from tests.support.asserts import assert_success
 from tests.support.inline import inline
 
+
 def click(session, element):
     return session.transport.send(
         "POST", "/session/{session_id}/element/{element_id}/click".format(
@@ -135,7 +136,8 @@
 
         function logEvent({type, target, currentTarget}) {
           log.innerHTML += "<p></p>";
-          log.lastElementChild.textContent = `${type} in ${target.id} (handled by ${currentTarget.id})`;
+          log.lastElementChild.textContent =
+              `${type} in ${target.id} (handled by ${currentTarget.id})`;
         }
 
         for (let ev of ["click", "mousedown", "mouseup"]) {
@@ -144,7 +146,9 @@
           body.addEventListener(ev, logEvent);
         }
 
-        over.addEventListener("mousedown", () => over.style.display = "none");
+        over.addEventListener("mousedown", function(mousedownEvent) {
+          over.style.display = "none";
+        });
         </script>
         """)
     over = session.find.css("#over", all=False)
diff --git a/webdriver/tests/execute_async_script/collections.py b/webdriver/tests/execute_async_script/collections.py
new file mode 100644
index 0000000..5affdc9
--- /dev/null
+++ b/webdriver/tests/execute_async_script/collections.py
@@ -0,0 +1,158 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from tests.support.inline import inline
+
+
+def execute_async_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/async".format(**vars(session)),
+        body)
+
+
+def test_arguments(session):
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        function func() {
+            return arguments;
+        }
+        resolve(func("foo", "bar"));
+        """)
+    assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve([1, 2]);
+        """)
+    assert_success(response, [1, 2])
+
+
+def test_file_list(session, tmpdir):
+    files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+    session.url = inline("<input type=file multiple>")
+    upload = session.find.css("input", all=False)
+    for file in files:
+        file.write("morn morn")
+        upload.send_keys(str(file))
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelector('input').files);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == len(files)
+    for expected, actual in zip(files, value):
+        assert isinstance(actual, dict)
+        assert "name" in actual
+        assert isinstance(actual["name"], basestring)
+        assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    html = session.find.css("html", all=False)
+    head = session.find.css("head", all=False)
+    body = session.find.css("body", all=False)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.all);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    # <html>, <head>, <body>, <p>, <p>
+    assert len(value) == 5
+
+    assert_same_element(session, html, value[0])
+    assert_same_element(session, head, value[1])
+    assert_same_element(session, body, value[2])
+    assert_same_element(session, ps[0], value[3])
+    assert_same_element(session, ps[1], value[4])
+
+
+def test_html_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.getElementsByTagName('p'));
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session):
+    session.url = inline("""
+        <form>
+            <input>
+            <input>
+        </form>
+        """)
+    inputs = session.find.css("input")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.forms[0].elements);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(inputs, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session):
+    session.url = inline("""
+        <select>
+            <option>
+            <option>
+        </select>
+        """)
+    options = session.find.css("option")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelector('select').options);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(options, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_node_list(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelectorAll('p'));
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
diff --git a/webdriver/tests/execute_async_script/user_prompts.py b/webdriver/tests/execute_async_script/user_prompts.py
index 03e1762..67f5512 100644
--- a/webdriver/tests/execute_async_script/user_prompts.py
+++ b/webdriver/tests/execute_async_script/user_prompts.py
@@ -7,52 +7,66 @@
 
 def test_handle_prompt_accept(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_async_script("window.alert('Hello');")
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_dismiss(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss"})}})
-    session.execute_async_script("window.alert('Hello');")
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_dismiss_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss and notify"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_accept_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept and notify"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_ignore(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "ignore"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     session.alert.dismiss()
 
 
 def test_handle_prompt_default(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_twice(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_async_script("window.alert('Hello');window.alert('Bye');")
+    value = session.execute_async_script("window.alert('Hello');window.alert('Bye');")
+    assert value is None
+    session.alert.dismiss()
     # The first alert has been accepted by the user prompt handler, the second one remains.
     # FIXME: this is how browsers currently work, but the spec should clarify if this is the
     #        expected behavior, see https://github.com/w3c/webdriver/issues/1153.
diff --git a/webdriver/tests/execute_script/collections.py b/webdriver/tests/execute_script/collections.py
new file mode 100644
index 0000000..d96c7fe
--- /dev/null
+++ b/webdriver/tests/execute_script/collections.py
@@ -0,0 +1,136 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from tests.support.inline import inline
+
+
+def execute_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/sync".format(**vars(session)),
+        body)
+
+
+def test_arguments(session):
+    response = execute_script(session, """
+        function func() {
+            return arguments;
+        }
+        return func("foo", "bar");
+        """)
+    assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+    response = execute_script(session, "return [1, 2]")
+    assert_success(response, [1, 2])
+
+
+def test_file_list(session, tmpdir):
+    files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+    session.url = inline("<input type=file multiple>")
+    upload = session.find.css("input", all=False)
+    for file in files:
+        file.write("morn morn")
+        upload.send_keys(str(file))
+
+    response = execute_script(session, "return document.querySelector('input').files")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == len(files)
+    for expected, actual in zip(files, value):
+        assert isinstance(actual, dict)
+        assert "name" in actual
+        assert isinstance(actual["name"], basestring)
+        assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    html = session.find.css("html", all=False)
+    head = session.find.css("head", all=False)
+    body = session.find.css("body", all=False)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.all")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    # <html>, <head>, <body>, <p>, <p>
+    assert len(value) == 5
+
+    assert_same_element(session, html, value[0])
+    assert_same_element(session, head, value[1])
+    assert_same_element(session, body, value[2])
+    assert_same_element(session, ps[0], value[3])
+    assert_same_element(session, ps[1], value[4])
+
+
+def test_html_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.getElementsByTagName('p')")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session):
+    session.url = inline("""
+        <form>
+            <input>
+            <input>
+        </form>
+        """)
+    inputs = session.find.css("input")
+
+    response = execute_script(session, "return document.forms[0].elements")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(inputs, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session):
+    session.url = inline("""
+        <select>
+            <option>
+            <option>
+        </select>
+        """)
+    options = session.find.css("option")
+
+    response = execute_script(session, "return document.querySelector('select').options")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(options, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_node_list(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.querySelectorAll('p')")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
diff --git a/webdriver/tests/execute_script/user_prompts.py b/webdriver/tests/execute_script/user_prompts.py
index 8d91bdd..befa8d8 100644
--- a/webdriver/tests/execute_script/user_prompts.py
+++ b/webdriver/tests/execute_script/user_prompts.py
@@ -7,52 +7,66 @@
 
 def test_handle_prompt_accept(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_script("window.alert('Hello');")
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_dismiss(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss"})}})
-    session.execute_script("window.alert('Hello');")
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_dismiss_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss and notify"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_accept_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept and notify"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_ignore(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "ignore"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     session.alert.dismiss()
 
 
 def test_handle_prompt_default(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_twice(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_script("window.alert('Hello');window.alert('Bye');")
+    value = session.execute_script("window.alert('Hello');window.alert('Bye');")
+    assert value is None
+    session.alert.dismiss()
     # The first alert has been accepted by the user prompt handler, the second one remains.
     # FIXME: this is how browsers currently work, but the spec should clarify if this is the
     #        expected behavior, see https://github.com/w3c/webdriver/issues/1153.
diff --git a/webdriver/tests/support/fixtures.py b/webdriver/tests/support/fixtures.py
index 6a20fec..dbf9f1d 100644
--- a/webdriver/tests/support/fixtures.py
+++ b/webdriver/tests/support/fixtures.py
@@ -76,6 +76,7 @@
     session.window_handle = current_window
 
 
+@ignore_exceptions
 def _switch_to_top_level_browsing_context(session):
     """If the current browsing context selected by WebDriver is a
     `<frame>` or an `<iframe>`, switch it back to the top-level
diff --git a/webmessaging/broadcastchannel/workers.html b/webmessaging/broadcastchannel/workers.html
index 861b550..1d05feb 100644
--- a/webmessaging/broadcastchannel/workers.html
+++ b/webmessaging/broadcastchannel/workers.html
@@ -121,4 +121,21 @@
   }
 }, 'BroadcastChannel created after a worker self.close()');
 
+async_test(t => {
+  function workerCode() {
+    close();
+    var bc = new BroadcastChannel('worker-test-after-close');
+    bc.postMessage(true);
+  }
+
+  var bc = new BroadcastChannel('worker-test-after-close');
+  bc.onmessage = function(e) {
+    assert_true(e.data, "BroadcastChannel created on worker shutdown.");
+    t.done();
+  }
+
+  var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], {type:"application/javascript"});
+  new Worker(URL.createObjectURL(workerBlob));
+}, 'BroadcastChannel used after a worker self.close()');
+
 </script>
diff --git a/webmessaging/message-channels/close.html b/webmessaging/message-channels/close.html
index cc3afd8..d975ea7 100644
--- a/webmessaging/message-channels/close.html
+++ b/webmessaging/message-channels/close.html
@@ -59,4 +59,10 @@
     c.port2.postMessage('DONE');
   }, 'Close in onmessage should not cancel inflight messages.');
 
+test(() => {
+    const c = new MessageChannel();
+    c.port1.close();
+    assert_throws("DataCloneError", () => self.postMessage(null, "*", [c.port1]));
+    self.postMessage(null, "*", [c.port2]);
+}, "close() detaches a MessagePort (but not the one its entangled with)");
 </script>
diff --git a/webrtc/RTCPeerConnection-track-stats.https.html b/webrtc/RTCPeerConnection-track-stats.https.html
index 2fa8d81..c21bd82 100644
--- a/webrtc/RTCPeerConnection-track-stats.https.html
+++ b/webrtc/RTCPeerConnection-track-stats.https.html
@@ -569,14 +569,20 @@
     return stats;
   }
 
-  // Returns a promise that is resolved when pc.iceConnectionState changes to
-  // 'completed'. This is when transport stats can be expected to have its
-  // selectedCandidatePairId defined.
+  // Returns a promise that is resolved when pc.iceConnectionState reaches the
+  // 'connected' or 'completed' state. This is when transport stats can be
+  // expected to have its selectedCandidatePairId defined.
   async function onIceConnectionStateCompleted(pc) {
+    if (pc.iceConnectionState == 'connected' ||
+        pc.iceConnectionState == 'completed') {
+      return Promise.resolve();
+    }
     let resolver = new Resolver();
     pc.oniceconnectionstatechange = e => {
-      if (pc.iceConnectionState == 'completed')
+      if (pc.iceConnectionState == 'connected' ||
+          pc.iceConnectionState == 'completed') {
         resolver.resolve();
+      }
     };
     return resolver.promise;
   }
diff --git a/websockets/Close-1000-reason.any.js b/websockets/Close-1000-reason.any.js
new file mode 100644
index 0000000..1517db2
--- /dev/null
+++ b/websockets/Close-1000-reason.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000, "Clean Close");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be opened");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Close-1000-reason.htm b/websockets/Close-1000-reason.htm
deleted file mode 100644
index 8424d78..0000000
--- a/websockets/Close-1000-reason.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close WebSocket - Code is 1000 and reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000, "Clean Close");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be opened");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Close-1000.any.js b/websockets/Close-1000.any.js
new file mode 100644
index 0000000..e052f23
--- /dev/null
+++ b/websockets/Close-1000.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be opened");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Close-1000.htm b/websockets/Close-1000.htm
deleted file mode 100644
index 1622109..0000000
--- a/websockets/Close-1000.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close WebSocket - Code is 1000</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be opened");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Close-Reason-124Bytes.any.js b/websockets/Close-Reason-124Bytes.any.js
new file mode 100644
index 0000000..97bc1bb
--- /dev/null
+++ b/websockets/Close-Reason-124Bytes.any.js
@@ -0,0 +1,15 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown");
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123";
+  assert_equals(reason.length, 124);
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket.close(1000, reason)
+  });
+  test.done();
+}), true);
diff --git a/websockets/Close-Reason-124Bytes.htm b/websockets/Close-Reason-124Bytes.htm
deleted file mode 100644
index 82a661e..0000000
--- a/websockets/Close-Reason-124Bytes.htm
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close WebSocket - Reason is more than 123 bytes long</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown");
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123";
-            assert_equals(reason.length, 124);
-            assert_throws("SYNTAX_ERR", function () { wsocket.close(1000, reason) });
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Close-reason-unpaired-surrogates.any.js b/websockets/Close-reason-unpaired-surrogates.any.js
new file mode 100644
index 0000000..119a32d
--- /dev/null
+++ b/websockets/Close-reason-unpaired-surrogates.any.js
@@ -0,0 +1,21 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed");
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+var replacementChar = "\uFFFD";
+var reason = "\uD807";
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000, reason);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be opened");
+  assert_equals(evt.reason, replacementChar, "reason replaced with replacement character");
+  testClose.done();
+}), true);
diff --git a/websockets/Close-reason-unpaired-surrogates.htm b/websockets/Close-reason-unpaired-surrogates.htm
deleted file mode 100644
index 9ad8d61..0000000
--- a/websockets/Close-reason-unpaired-surrogates.htm
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close WebSocket - Reason with unpaired surrogates</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed");
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-        var replacementChar = "\uFFFD";
-        var reason = "\uD807";
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000, reason);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be opened");
-            assert_equals(evt.reason, replacementChar, "reason replaced with replacement character");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Close-undefined.any.js b/websockets/Close-undefined.any.js
new file mode 100644
index 0000000..67bc9b2
--- /dev/null
+++ b/websockets/Close-undefined.any.js
@@ -0,0 +1,11 @@
+// META: script=websocket.sub.js
+
+var test = async_test();
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  wsocket.close(undefined);
+  test.done();
+}), true);
diff --git a/websockets/Close-undefined.htm b/websockets/Close-undefined.htm
deleted file mode 100644
index d6c89afb..0000000
--- a/websockets/Close-undefined.htm
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close WebSocket - Code is undefined</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test();
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            wsocket.close(undefined);
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Create-Secure-extensions-empty.any.js b/websockets/Create-Secure-extensions-empty.any.js
new file mode 100644
index 0000000..82b000d
--- /dev/null
+++ b/websockets/Create-Secure-extensions-empty.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.extensions, "", "extensions should be empty");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be closed");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-extensions-empty.htm b/websockets/Create-Secure-extensions-empty.htm
deleted file mode 100644
index 2f9f797..0000000
--- a/websockets/Create-Secure-extensions-empty.htm
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.extensions, "", "extensions should be empty");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be closed");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-url-with-space.any.js b/websockets/Create-Secure-url-with-space.any.js
new file mode 100644
index 0000000..94265c6
--- /dev/null
+++ b/websockets/Create-Secure-url-with-space.any.js
@@ -0,0 +1,9 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  var spaceUrl = "web platform.test";
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketWithSpaceInUrl(spaceUrl)
+  });
+}, "W3C WebSocket API - Create Secure WebSocket - Pass a URL with a space - SYNTAX_ERR should be thrown")
diff --git a/websockets/Create-Secure-url-with-space.htm b/websockets/Create-Secure-url-with-space.htm
deleted file mode 100644
index d2dfe1b..0000000
--- a/websockets/Create-Secure-url-with-space.htm
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket - url with space</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            var spaceUrl = "web platform.test";
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketWithSpaceInUrl(spaceUrl) });
-        }, "W3C WebSocket API - Create Secure WebSocket - Pass a URL with a space - SYNTAX_ERR should be thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-valid-url-array-protocols.any.js b/websockets/Create-Secure-valid-url-array-protocols.any.js
new file mode 100644
index 0000000..fcaf8a3
--- /dev/null
+++ b/websockets/Create-Secure-valid-url-array-protocols.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and array of protocol strings - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, true);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-valid-url-array-protocols.htm b/websockets/Create-Secure-valid-url-array-protocols.htm
deleted file mode 100644
index d61a429..0000000
--- a/websockets/Create-Secure-valid-url-array-protocols.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket with valid url and array of protocols</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and array of protocol strings - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, true);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-valid-url-binaryType-blob.any.js b/websockets/Create-Secure-valid-url-binaryType-blob.any.js
new file mode 100644
index 0000000..fed88f5
--- /dev/null
+++ b/websockets/Create-Secure-valid-url-binaryType-blob.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.binaryType, "blob", "binaryType should be set to Blob");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-valid-url-binaryType-blob.htm b/websockets/Create-Secure-valid-url-binaryType-blob.htm
deleted file mode 100644
index 0261fb4..0000000
--- a/websockets/Create-Secure-valid-url-binaryType-blob.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket - binaryType set correctly</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.binaryType, "blob", "binaryType should be set to Blob");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-valid-url-protocol-setCorrectly.any.js b/websockets/Create-Secure-valid-url-protocol-setCorrectly.any.js
new file mode 100644
index 0000000..7ecd295
--- /dev/null
+++ b/websockets/Create-Secure-valid-url-protocol-setCorrectly.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - protocol should be set correctly - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.protocol, "echo", "protocol should be set to echo");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-valid-url-protocol-setCorrectly.htm b/websockets/Create-Secure-valid-url-protocol-setCorrectly.htm
deleted file mode 100644
index a7f1510..0000000
--- a/websockets/Create-Secure-valid-url-protocol-setCorrectly.htm
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket and verify if protocol is set correctly after connection</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-            var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - protocol should be set correctly - Connection should be opened");
-            var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - Connection should be closed");
-
-            var wsocket = CreateWebSocket(true, true, false);
-            var isOpenCalled = false;
-
-            wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-                assert_equals(wsocket.protocol, "echo", "protocol should be set to echo");
-                wsocket.close();
-                isOpenCalled = true;
-                testOpen.done();
-            }), true);
-
-            wsocket.addEventListener('close', testClose.step_func(function (evt) {
-                assert_true(isOpenCalled, "WebSocket connection should be open");
-                assert_equals(evt.wasClean, true, "wasClean should be true");
-                    testClose.done();
-            }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-valid-url-protocol-string.any.js b/websockets/Create-Secure-valid-url-protocol-string.any.js
new file mode 100644
index 0000000..59c77c6
--- /dev/null
+++ b/websockets/Create-Secure-valid-url-protocol-string.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Check readyState is 1");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-valid-url-protocol-string.htm b/websockets/Create-Secure-valid-url-protocol-string.htm
deleted file mode 100644
index 7250b4e..0000000
--- a/websockets/Create-Secure-valid-url-protocol-string.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket with valid url and protocol string</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Check readyState is 1");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL and protocol string - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, true, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-valid-url.any.js b/websockets/Create-Secure-valid-url.any.js
new file mode 100644
index 0000000..6f1229e
--- /dev/null
+++ b/websockets/Create-Secure-valid-url.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL  - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-Secure-valid-url.htm b/websockets/Create-Secure-valid-url.htm
deleted file mode 100644
index 1fe6c0f..0000000
--- a/websockets/Create-Secure-valid-url.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket with valid url</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL  - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Pass a valid URL - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-Secure-verify-url-set-non-default-port.any.js b/websockets/Create-Secure-verify-url-set-non-default-port.any.js
new file mode 100644
index 0000000..755dbe2
--- /dev/null
+++ b/websockets/Create-Secure-verify-url-set-non-default-port.any.js
@@ -0,0 +1,7 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var urlNonDefaultPort = "wss://" + __SERVER__NAME + ":" + __NEW__SECURE__PORT + "/" + __PATH;
+  var wsocket = new WebSocket(urlNonDefaultPort);
+  assert_equals(wsocket.url, urlNonDefaultPort, "wsocket.url is set correctly");
+}, "W3C WebSocket API - Create Secure WebSocket - wsocket.url should be set correctly")
diff --git a/websockets/Create-Secure-verify-url-set-non-default-port.htm b/websockets/Create-Secure-verify-url-set-non-default-port.htm
deleted file mode 100644
index 63e49f4..0000000
--- a/websockets/Create-Secure-verify-url-set-non-default-port.htm
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket - wsocket.url is set correctly - non default port</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var urlNonDefaultPort = "wss://" + __SERVER__NAME + ":" + __NEW__SECURE__PORT + "/" + __PATH;
-            var wsocket = new WebSocket(urlNonDefaultPort);
-            assert_equals(wsocket.url, urlNonDefaultPort, "wsocket.url is set correctly");
-        }, "W3C WebSocket API - Create Secure WebSocket - wsocket.url should be set correctly")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-asciiSep-protocol-string.any.js b/websockets/Create-asciiSep-protocol-string.any.js
new file mode 100644
index 0000000..cb3c3e4
--- /dev/null
+++ b/websockets/Create-asciiSep-protocol-string.any.js
@@ -0,0 +1,9 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var asciiWithSep = "/echo";
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketWithAsciiSep(asciiWithSep)
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with an ascii separator character - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-asciiSep-protocol-string.htm b/websockets/Create-asciiSep-protocol-string.htm
deleted file mode 100644
index 0d0480e..0000000
--- a/websockets/Create-asciiSep-protocol-string.htm
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - ascii protocol string with separator</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        if(window.WebSocket) {
-            test(function () {
-                var asciiWithSep = "/echo";
-                var wsocket;
-                assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketWithAsciiSep(asciiWithSep) });
-            }, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with an ascii separator character - SYNTAX_ERR is thrown")
-        }
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-blocked-port.any.js b/websockets/Create-blocked-port.any.js
new file mode 100644
index 0000000..f0dbc3f
--- /dev/null
+++ b/websockets/Create-blocked-port.any.js
@@ -0,0 +1,81 @@
+// META: script=websocket.sub.js
+
+async_test(t => {
+  const ws = CreateWebSocketWithBlockedPort(__PORT)
+  ws.onerror = t.unreached_func()
+  ws.onopen = t.step_func_done()
+}, 'Basic check');
+// list of bad ports according to
+// https://fetch.spec.whatwg.org/#port-blocking
+[
+  1, // tcpmux
+  7, // echo
+  9, // discard
+  11, // systat
+  13, // daytime
+  15, // netstat
+  17, // qotd
+  19, // chargen
+  20, // ftp-data
+  21, // ftp
+  22, // ssh
+  23, // telnet
+  25, // smtp
+  37, // time
+  42, // name
+  43, // nicname
+  53, // domain
+  77, // priv-rjs
+  79, // finger
+  87, // ttylink
+  95, // supdup
+  101, // hostriame
+  102, // iso-tsap
+  103, // gppitnp
+  104, // acr-nema
+  109, // pop2
+  110, // pop3
+  111, // sunrpc
+  113, // auth
+  115, // sftp
+  117, // uucp-path
+  119, // nntp
+  123, // ntp
+  135, // loc-srv / epmap
+  139, // netbios
+  143, // imap2
+  179, // bgp
+  389, // ldap
+  465, // smtp+ssl
+  512, // print / exec
+  513, // login
+  514, // shell
+  515, // printer
+  526, // tempo
+  530, // courier
+  531, // chat
+  532, // netnews
+  540, // uucp
+  556, // remotefs
+  563, // nntp+ssl
+  587, // smtp
+  601, // syslog-conn
+  636, // ldap+ssl
+  993, // imap+ssl
+  995, // pop3+ssl
+  2049, // nfs
+  3659, // apple-sasl
+  4045, // lockd
+  6000, // x11
+  6665, // irc (alternate)
+  6666, // irc (alternate)
+  6667, // irc (default)
+  6668, // irc (alternate)
+  6669, // irc (alternate)
+].forEach(blockedPort => {
+  async_test(t => {
+    const ws = CreateWebSocketWithBlockedPort(blockedPort)
+    ws.onerror = t.step_func_done()
+    ws.onopen = t.unreached_func()
+  }, "WebSocket blocked port test " + blockedPort)
+})
diff --git a/websockets/Create-blocked-port.htm b/websockets/Create-blocked-port.htm
deleted file mode 100644
index aafaee6..0000000
--- a/websockets/Create-blocked-port.htm
+++ /dev/null
@@ -1,93 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>WebSocket API - blocked port</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script>
-            async_test(t => {
-              const ws = CreateWebSocketWithBlockedPort(__PORT)
-              ws.onerror = t.unreached_func()
-              ws.onopen = t.step_func_done()
-            }, 'Basic check');
-            // list of bad ports according to
-            // https://fetch.spec.whatwg.org/#port-blocking
-            [
-                1,    // tcpmux
-                7,    // echo
-                9,    // discard
-                11,   // systat
-                13,   // daytime
-                15,   // netstat
-                17,   // qotd
-                19,   // chargen
-                20,   // ftp-data
-                21,   // ftp
-                22,   // ssh
-                23,   // telnet
-                25,   // smtp
-                37,   // time
-                42,   // name
-                43,   // nicname
-                53,   // domain
-                77,   // priv-rjs
-                79,   // finger
-                87,   // ttylink
-                95,   // supdup
-                101,  // hostriame
-                102,  // iso-tsap
-                103,  // gppitnp
-                104,  // acr-nema
-                109,  // pop2
-                110,  // pop3
-                111,  // sunrpc
-                113,  // auth
-                115,  // sftp
-                117,  // uucp-path
-                119,  // nntp
-                123,  // ntp
-                135,  // loc-srv / epmap
-                139,  // netbios
-                143,  // imap2
-                179,  // bgp
-                389,  // ldap
-                465,  // smtp+ssl
-                512,  // print / exec
-                513,  // login
-                514,  // shell
-                515,  // printer
-                526,  // tempo
-                530,  // courier
-                531,  // chat
-                532,  // netnews
-                540,  // uucp
-                556,  // remotefs
-                563,  // nntp+ssl
-                587,  // smtp
-                601,  // syslog-conn
-                636,  // ldap+ssl
-                993,  // imap+ssl
-                995,  // pop3+ssl
-                2049, // nfs
-                3659, // apple-sasl
-                4045, // lockd
-                6000, // x11
-                6665, // irc (alternate)
-                6666, // irc (alternate)
-                6667, // irc (default)
-                6668, // irc (alternate)
-                6669, // irc (alternate)
-            ].forEach(blockedPort => {
-              async_test(t => {
-                const ws = CreateWebSocketWithBlockedPort(blockedPort)
-                ws.onerror = t.step_func_done()
-                ws.onopen = t.unreached_func()
-              }, "WebSocket blocked port test " + blockedPort)
-            })
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-invalid-urls.any.js b/websockets/Create-invalid-urls.any.js
new file mode 100644
index 0000000..5ae25a2
--- /dev/null
+++ b/websockets/Create-invalid-urls.any.js
@@ -0,0 +1,32 @@
+// META: script=websocket.sub.js
+
+var wsocket;
+test(function() {
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = new WebSocket("/echo")
+  });
+}, "Url is /echo - should throw SYNTAX_ERR");
+
+test(function() {
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = new WebSocket("mailto:microsoft@microsoft.com")
+  });
+}, "Url is a mail address - should throw SYNTAX_ERR");
+
+test(function() {
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = new WebSocket("about:blank")
+  });
+}, "Url is about:blank - should throw SYNTAX_ERR");
+
+test(function() {
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = new WebSocket("?test")
+  });
+}, "Url is ?test - should throw SYNTAX_ERR");
+
+test(function() {
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = new WebSocket("#test")
+  });
+}, "Url is #test - should throw SYNTAX_ERR");
diff --git a/websockets/Create-invalid-urls.htm b/websockets/Create-invalid-urls.htm
deleted file mode 100644
index c4a4c79..0000000
--- a/websockets/Create-invalid-urls.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - pass in list of invalid urls</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        var wsocket;
-        test(function() {
-            assert_throws("SYNTAX_ERR", function () { wsocket = new WebSocket("/echo") });
-        }, "Url is /echo - should throw SYNTAX_ERR");
-
-        test(function () {
-            assert_throws("SYNTAX_ERR", function () { wsocket = new WebSocket("mailto:microsoft@microsoft.com") });
-        }, "Url is a mail address - should throw SYNTAX_ERR");
-
-        test(function () {
-            assert_throws("SYNTAX_ERR", function () { wsocket = new WebSocket("about:blank") });
-        }, "Url is about:blank - should throw SYNTAX_ERR");
-
-        test(function () {
-            assert_throws("SYNTAX_ERR", function () { wsocket = new WebSocket("?test") });
-        }, "Url is ?test - should throw SYNTAX_ERR");
-
-        test(function () {
-            assert_throws("SYNTAX_ERR", function () { wsocket = new WebSocket("#test") });
-        }, "Url is #test - should throw SYNTAX_ERR");
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-non-absolute-url.any.js b/websockets/Create-non-absolute-url.any.js
new file mode 100644
index 0000000..369557e
--- /dev/null
+++ b/websockets/Create-non-absolute-url.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketNonAbsolute()
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a non absolute URL - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-non-absolute-url.htm b/websockets/Create-non-absolute-url.htm
deleted file mode 100644
index 2ed8ece..0000000
--- a/websockets/Create-non-absolute-url.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - non absolute url</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketNonAbsolute() });
-        }, "W3C WebSocket API - Create WebSocket - Pass a non absolute URL - SYNTAX_ERR is thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-nonAscii-protocol-string.any.js b/websockets/Create-nonAscii-protocol-string.any.js
new file mode 100644
index 0000000..39be9f4
--- /dev/null
+++ b/websockets/Create-nonAscii-protocol-string.any.js
@@ -0,0 +1,9 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var nonAsciiProtocol = "\u0080echo";
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketNonAsciiProtocol(nonAsciiProtocol)
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with non-ascii values - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-nonAscii-protocol-string.htm b/websockets/Create-nonAscii-protocol-string.htm
deleted file mode 100644
index fd34018..0000000
--- a/websockets/Create-nonAscii-protocol-string.htm
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - non ascii protocol string</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        if(window.WebSocket) {
-            test(function () {
-                var nonAsciiProtocol = "\u0080echo";
-                var wsocket;
-                assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketNonAsciiProtocol(nonAsciiProtocol) });
-            }, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with non-ascii values - SYNTAX_ERR is thrown")
-        }
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-on-worker-shutdown.any.js b/websockets/Create-on-worker-shutdown.any.js
new file mode 100644
index 0000000..cb3eff8
--- /dev/null
+++ b/websockets/Create-on-worker-shutdown.any.js
@@ -0,0 +1,19 @@
+// META: script=websocket.sub.js
+
+async_test(t => {
+  function workerCode() {
+    close();
+    var ws = new WebSocket(self.location.origin.replace('http', 'ws'));
+    postMessage(ws.readyState == WebSocket.CONNECTING);
+  }
+
+  var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], {
+    type: "application/javascript"
+  });
+
+  var w = new Worker(URL.createObjectURL(workerBlob));
+  w.onmessage = function(e) {
+    assert_true(e.data, "WebSocket created on worker shutdown.");
+    t.done();
+  }
+}, 'WebSocket created after a worker self.close()');
diff --git a/websockets/Create-on-worker-shutdown.html b/websockets/Create-on-worker-shutdown.html
deleted file mode 100644
index 2bdf549..0000000
--- a/websockets/Create-on-worker-shutdown.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - on a worker after self.close()</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-async_test(t => {
-  function workerCode() {
-    close();
-    var ws = new WebSocket(self.location.origin.replace('http', 'ws'));
-    postMessage(ws.readyState == WebSocket.CONNECTING);
-  }
-
-  var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], {type:"application/javascript"});
-
-  var w = new Worker(URL.createObjectURL(workerBlob));
-  w.onmessage = function(e) {
-    assert_true(e.data, "WebSocket created on worker shutdown.");
-    t.done();
-  }
-}, 'WebSocket created after a worker self.close()');
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-protocol-with-space.any.js b/websockets/Create-protocol-with-space.any.js
new file mode 100644
index 0000000..b3c14d8
--- /dev/null
+++ b/websockets/Create-protocol-with-space.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketWithSpaceInProtocol("ec ho")
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with a space in it - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-protocol-with-space.htm b/websockets/Create-protocol-with-space.htm
deleted file mode 100644
index 023a498..0000000
--- a/websockets/Create-protocol-with-space.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - protocol with space</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketWithSpaceInProtocol("ec ho") });
-        }, "W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string with a space in it - SYNTAX_ERR is thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-protocols-repeated-case-insensitive.any.js b/websockets/Create-protocols-repeated-case-insensitive.any.js
new file mode 100644
index 0000000..16f9975
--- /dev/null
+++ b/websockets/Create-protocols-repeated-case-insensitive.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketWithRepeatedProtocolsCaseInsensitive()
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values but different case - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-protocols-repeated-case-insensitive.htm b/websockets/Create-protocols-repeated-case-insensitive.htm
deleted file mode 100644
index 47225ef..0000000
--- a/websockets/Create-protocols-repeated-case-insensitive.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - repeated protocols with different case</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketWithRepeatedProtocolsCaseInsensitive() });
-        }, "W3C WebSocket API - Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values but different case - SYNTAX_ERR is thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-protocols-repeated.any.js b/websockets/Create-protocols-repeated.any.js
new file mode 100644
index 0000000..624d453
--- /dev/null
+++ b/websockets/Create-protocols-repeated.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketWithRepeatedProtocols()
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-protocols-repeated.htm b/websockets/Create-protocols-repeated.htm
deleted file mode 100644
index 6a62bca..0000000
--- a/websockets/Create-protocols-repeated.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - repeated protocols</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketWithRepeatedProtocols() });
-        }, "W3C WebSocket API - Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values - SYNTAX_ERR is thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-valid-url-array-protocols.any.js b/websockets/Create-valid-url-array-protocols.any.js
new file mode 100644
index 0000000..dde0303
--- /dev/null
+++ b/websockets/Create-valid-url-array-protocols.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false, true);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-valid-url-array-protocols.htm b/websockets/Create-valid-url-array-protocols.htm
deleted file mode 100644
index 3ebf3f5..0000000
--- a/websockets/Create-valid-url-array-protocols.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket with valid url and array of protocols</title>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed");
-
-        var wsocket = CreateWebSocket(false, false, true);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Create-valid-url-protocol-empty.any.js b/websockets/Create-valid-url-protocol-empty.any.js
new file mode 100644
index 0000000..8682e4a
--- /dev/null
+++ b/websockets/Create-valid-url-protocol-empty.any.js
@@ -0,0 +1,7 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket = CreateWebSocket(false, true, false);
+  assert_equals(wsocket.protocol, "", "protocol should be empty");
+  wsocket.close();
+}, "W3C WebSocket API - Create WebSocket - wsocket.protocol should be empty before connection is established")
diff --git a/websockets/Create-valid-url-protocol-empty.htm b/websockets/Create-valid-url-protocol-empty.htm
deleted file mode 100644
index 9146937..0000000
--- a/websockets/Create-valid-url-protocol-empty.htm
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - wsocket.protocol is empty before connection is established</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket = CreateWebSocket(false, true, false);
-            assert_equals(wsocket.protocol, "", "protocol should be empty");
-            wsocket.close();
-        }, "W3C WebSocket API - Create WebSocket - wsocket.protocol should be empty before connection is established")
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-valid-url-protocol.any.js b/websockets/Create-valid-url-protocol.any.js
new file mode 100644
index 0000000..85e870f
--- /dev/null
+++ b/websockets/Create-valid-url-protocol.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-valid-url-protocol.htm b/websockets/Create-valid-url-protocol.htm
deleted file mode 100644
index 0a6dd97..0000000
--- a/websockets/Create-valid-url-protocol.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket with a valid url and protocol string</title>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL and a protocol string - Connection should be closed");
-
-        var wsocket = CreateWebSocket(false, true, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-valid-url.any.js b/websockets/Create-valid-url.any.js
new file mode 100644
index 0000000..9a43dcc
--- /dev/null
+++ b/websockets/Create-valid-url.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Create-valid-url.htm b/websockets/Create-valid-url.htm
deleted file mode 100644
index e74a7bf..0000000
--- a/websockets/Create-valid-url.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket with valid url</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - Pass a valid URL - Connection should be closed");
-
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-verify-url-set-non-default-port.any.js b/websockets/Create-verify-url-set-non-default-port.any.js
new file mode 100644
index 0000000..5548fd1
--- /dev/null
+++ b/websockets/Create-verify-url-set-non-default-port.any.js
@@ -0,0 +1,7 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var urlNonDefaultPort = "ws://" + __SERVER__NAME + ":" + __NEW__PORT + "/" + __PATH;
+  var wsocket = new WebSocket(urlNonDefaultPort);
+  assert_equals(wsocket.url, urlNonDefaultPort, "wsocket.url is set correctly");
+}, "W3C WebSocket API - Create WebSocket - wsocket.url should be set correctly");
diff --git a/websockets/Create-verify-url-set-non-default-port.htm b/websockets/Create-verify-url-set-non-default-port.htm
deleted file mode 100644
index 181794d..0000000
--- a/websockets/Create-verify-url-set-non-default-port.htm
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - wsocket.url is set correctly - non default port</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var urlNonDefaultPort = "ws://" + __SERVER__NAME + ":" + __NEW__PORT + "/" + __PATH;
-            var wsocket = new WebSocket(urlNonDefaultPort);
-            assert_equals(wsocket.url, urlNonDefaultPort, "wsocket.url is set correctly");
-        }, "W3C WebSocket API - Create WebSocket - wsocket.url should be set correctly");
-    </script>
-</body>
-</html>
diff --git a/websockets/Create-wrong-scheme.any.js b/websockets/Create-wrong-scheme.any.js
new file mode 100644
index 0000000..506f81c
--- /dev/null
+++ b/websockets/Create-wrong-scheme.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket;
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket = CreateWebSocketNonWsScheme()
+  });
+}, "W3C WebSocket API - Create WebSocket - Pass a URL with a non ws/wss scheme - SYNTAX_ERR is thrown")
diff --git a/websockets/Create-wrong-scheme.htm b/websockets/Create-wrong-scheme.htm
deleted file mode 100644
index a0a9820..0000000
--- a/websockets/Create-wrong-scheme.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create WebSocket - non ws/wss scheme in url</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket;
-            assert_throws("SYNTAX_ERR", function () { wsocket = CreateWebSocketNonWsScheme() });
-        }, "W3C WebSocket API - Create WebSocket - Pass a URL with a non ws/wss scheme - SYNTAX_ERR is thrown")
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Close-1000-reason.any.js b/websockets/Secure-Close-1000-reason.any.js
new file mode 100644
index 0000000..4d3f67c
--- /dev/null
+++ b/websockets/Secure-Close-1000-reason.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000, "Clean Close");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-1000-reason.htm b/websockets/Secure-Close-1000-reason.htm
deleted file mode 100644
index 3dead6b..0000000
--- a/websockets/Secure-Close-1000-reason.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 1000 and reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000, "Clean Close");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-1000-verify-code.any.js b/websockets/Secure-Close-1000-verify-code.any.js
new file mode 100644
index 0000000..87ba407
--- /dev/null
+++ b/websockets/Secure-Close-1000-verify-code.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - event.code == 1000 and event.reason = 'Clean Close'");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000, "Clean Close");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.code, 1000, "CloseEvent.code should be 1000");
+  assert_equals(evt.reason, "Clean Close", "CloseEvent.reason should be the same as the reason sent in close");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-1000-verify-code.htm b/websockets/Secure-Close-1000-verify-code.htm
deleted file mode 100644
index 0cac622..0000000
--- a/websockets/Secure-Close-1000-verify-code.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 1000 - verify code in CloseEvent is 1000</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000, reason) - event.code == 1000 and event.reason = 'Clean Close'");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000, "Clean Close");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.code, 1000, "CloseEvent.code should be 1000");
-            assert_equals(evt.reason, "Clean Close", "CloseEvent.reason should be the same as the reason sent in close");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-1000.any.js b/websockets/Secure-Close-1000.any.js
new file mode 100644
index 0000000..67f4e05
--- /dev/null
+++ b/websockets/Secure-Close-1000.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-1000.htm b/websockets/Secure-Close-1000.htm
deleted file mode 100644
index 3da8630..0000000
--- a/websockets/Secure-Close-1000.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 1000</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-1005-verify-code.any.js b/websockets/Secure-Close-1005-verify-code.any.js
new file mode 100644
index 0000000..a7c72ea
--- /dev/null
+++ b/websockets/Secure-Close-1005-verify-code.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close() - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close() - return close code is 1005 - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.code, 1005, "CloseEvent.code should be 1005");
+  assert_equals(evt.reason, "", "CloseEvent.reason should be empty");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-1005-verify-code.htm b/websockets/Secure-Close-1005-verify-code.htm
deleted file mode 100644
index 6414b77..0000000
--- a/websockets/Secure-Close-1005-verify-code.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - verify return code is 1005</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close() - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close() - return close code is 1005 - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.code, 1005, "CloseEvent.code should be 1005");
-            assert_equals(evt.reason, "", "CloseEvent.reason should be empty");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-1005.any.js b/websockets/Secure-Close-1005.any.js
new file mode 100644
index 0000000..ddb2c18
--- /dev/null
+++ b/websockets/Secure-Close-1005.any.js
@@ -0,0 +1,13 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1005) - see '7.1.5.  The WebSocket Connection Close Code' in http://www.ietf.org/rfc/rfc6455.txt");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  assert_throws("INVALID_ACCESS_ERR", function() {
+    wsocket.close(1005, "1005 - reserved code")
+  });
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-1005.htm b/websockets/Secure-Close-1005.htm
deleted file mode 100644
index de8f51f..0000000
--- a/websockets/Secure-Close-1005.htm
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 1005</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(1005) - see '7.1.5.  The WebSocket Connection Close Code' in http://www.ietf.org/rfc/rfc6455.txt");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            assert_throws("INVALID_ACCESS_ERR", function () { wsocket.close(1005, "1005 - reserved code") });
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-2999-reason.any.js b/websockets/Secure-Close-2999-reason.any.js
new file mode 100644
index 0000000..0fa198e
--- /dev/null
+++ b/websockets/Secure-Close-2999-reason.any.js
@@ -0,0 +1,12 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(2999, reason) - INVALID_ACCESS_ERR is thrown");
+
+var wsocket = CreateWebSocket(true, false, false);
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  assert_throws("INVALID_ACCESS_ERR", function() {
+    wsocket.close(2999, "Close not in range 3000-4999")
+  });
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-2999-reason.htm b/websockets/Secure-Close-2999-reason.htm
deleted file mode 100644
index 223fc62..0000000
--- a/websockets/Secure-Close-2999-reason.htm
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 2999 and reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(2999, reason) - INVALID_ACCESS_ERR is thrown");
-
-        var wsocket = CreateWebSocket(true, false, false);
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            assert_throws("INVALID_ACCESS_ERR", function () { wsocket.close(2999, "Close not in range 3000-4999") });
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-3000-reason.any.js b/websockets/Secure-Close-3000-reason.any.js
new file mode 100644
index 0000000..6640ddc
--- /dev/null
+++ b/websockets/Secure-Close-3000-reason.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(3000, "Clean Close with code - 3000");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-3000-reason.htm b/websockets/Secure-Close-3000-reason.htm
deleted file mode 100644
index 80a8be7..0000000
--- a/websockets/Secure-Close-3000-reason.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 3000 and reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(3000, "Clean Close with code - 3000");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-3000-verify-code.any.js b/websockets/Secure-Close-3000-verify-code.any.js
new file mode 100644
index 0000000..5b122d4
--- /dev/null
+++ b/websockets/Secure-Close-3000-verify-code.any.js
@@ -0,0 +1,19 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - verify return code is 3000 - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(3000, "Clean Close");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.code, 3000, "CloseEvent.code should be 3000");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-3000-verify-code.htm b/websockets/Secure-Close-3000-verify-code.htm
deleted file mode 100644
index 9d385d9..0000000
--- a/websockets/Secure-Close-3000-verify-code.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 3000 - verify code in CloseEvent is 3000</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(3000, reason) - verify return code is 3000 - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(3000, "Clean Close");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.code, 3000, "CloseEvent.code should be 3000");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-4999-reason.any.js b/websockets/Secure-Close-4999-reason.any.js
new file mode 100644
index 0000000..d57899e
--- /dev/null
+++ b/websockets/Secure-Close-4999-reason.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(4999, reason) - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(4999, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(3000, "Clean Close with code - 4999");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-4999-reason.htm b/websockets/Secure-Close-4999-reason.htm
deleted file mode 100644
index a6ea069..0000000
--- a/websockets/Secure-Close-4999-reason.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is 4999 and reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(4999, reason) - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(4999, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(3000, "Clean Close with code - 4999");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-Reason-124Bytes.any.js b/websockets/Secure-Close-Reason-124Bytes.any.js
new file mode 100644
index 0000000..826cb6e
--- /dev/null
+++ b/websockets/Secure-Close-Reason-124Bytes.any.js
@@ -0,0 +1,15 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123";
+  assert_equals(reason.length, 124);
+  assert_throws("SYNTAX_ERR", function() {
+    wsocket.close(1000, reason)
+  });
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-Reason-124Bytes.htm b/websockets/Secure-Close-Reason-124Bytes.htm
deleted file mode 100644
index 94feb0f..0000000
--- a/websockets/Secure-Close-Reason-124Bytes.htm
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Reason is more than 123 bytes long</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123";
-            assert_equals(reason.length, 124);
-            assert_throws("SYNTAX_ERR", function () { wsocket.close(1000, reason) });
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-Reason-Unpaired-surrogates.any.js b/websockets/Secure-Close-Reason-Unpaired-surrogates.any.js
new file mode 100644
index 0000000..fdc62c4
--- /dev/null
+++ b/websockets/Secure-Close-Reason-Unpaired-surrogates.any.js
@@ -0,0 +1,21 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+var replacementChar = "\uFFFD";
+var reason = "\uD807";
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close(1000, reason);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be opened");
+  assert_equals(evt.reason, replacementChar, "reason replaced with replacement character");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-Reason-Unpaired-surrogates.htm b/websockets/Secure-Close-Reason-Unpaired-surrogates.htm
deleted file mode 100644
index 6947173..0000000
--- a/websockets/Secure-Close-Reason-Unpaired-surrogates.htm
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Reason with unpaired surrogates</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-        var replacementChar = "\uFFFD";
-        var reason = "\uD807";
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close(1000, reason);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be opened");
-            assert_equals(evt.reason, replacementChar, "reason replaced with replacement character");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
-
diff --git a/websockets/Secure-Close-onlyReason.any.js b/websockets/Secure-Close-onlyReason.any.js
new file mode 100644
index 0000000..79f79b5
--- /dev/null
+++ b/websockets/Secure-Close-onlyReason.any.js
@@ -0,0 +1,12 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(only reason) - INVALID_ACCESS_ERR is thrown");
+
+var wsocket = CreateWebSocket(true, false, false);
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  assert_throws("INVALID_ACCESS_ERR", function() {
+    wsocket.close("Close with only reason")
+  });
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-onlyReason.htm b/websockets/Secure-Close-onlyReason.htm
deleted file mode 100644
index 278d7aa..0000000
--- a/websockets/Secure-Close-onlyReason.htm
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Only reason</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - close(only reason) - INVALID_ACCESS_ERR is thrown");
-
-        var wsocket = CreateWebSocket(true, false, false);
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            assert_throws("INVALID_ACCESS_ERR", function () { wsocket.close("Close with only reason") });
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-readyState-Closed.any.js b/websockets/Secure-Close-readyState-Closed.any.js
new file mode 100644
index 0000000..3279744
--- /dev/null
+++ b/websockets/Secure-Close-readyState-Closed.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.close();
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-readyState-Closed.htm b/websockets/Secure-Close-readyState-Closed.htm
deleted file mode 100644
index 3ed38a8..0000000
--- a/websockets/Secure-Close-readyState-Closed.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - verify readyState is 3 when onclose is fired</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.close();
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-readyState-Closing.any.js b/websockets/Secure-Close-readyState-Closing.any.js
new file mode 100644
index 0000000..b183474
--- /dev/null
+++ b/websockets/Secure-Close-readyState-Closing.any.js
@@ -0,0 +1,12 @@
+// META: script=websocket.sub.js
+
+var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - readyState should be in CLOSING state just before onclose is called");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  wsocket.close();
+  assert_equals(wsocket.readyState, 2, "readyState should be 2(CLOSING)");
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-readyState-Closing.htm b/websockets/Secure-Close-readyState-Closing.htm
deleted file mode 100644
index 1048396..0000000
--- a/websockets/Secure-Close-readyState-Closing.htm
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - verify readyState is 2 before onclose is fired</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test("W3C WebSocket API - Create Secure WebSocket - Close the Connection - readyState should be in CLOSING state just before onclose is called");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            wsocket.close();
-            assert_equals(wsocket.readyState, 2, "readyState should be 2(CLOSING)");
-            test.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-server-initiated-close.any.js b/websockets/Secure-Close-server-initiated-close.any.js
new file mode 100644
index 0000000..8531b31
--- /dev/null
+++ b/websockets/Secure-Close-server-initiated-close.any.js
@@ -0,0 +1,20 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Server initiated Close - Client sends back a CLOSE - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Server initiated Close - Client sends back a CLOSE - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send("Goodbye");
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+  assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Close-server-initiated-close.htm b/websockets/Secure-Close-server-initiated-close.htm
deleted file mode 100644
index 4262754..0000000
--- a/websockets/Secure-Close-server-initiated-close.htm
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Server Initiated close</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create Secure WebSocket - Server initiated Close - Client sends back a CLOSE - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create Secure WebSocket - Server initiated Close - Client sends back a CLOSE - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send("Goodbye");
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
-            assert_equals(evt.wasClean, true, "wasClean should be TRUE");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Close-undefined.any.js b/websockets/Secure-Close-undefined.any.js
new file mode 100644
index 0000000..31b36e4
--- /dev/null
+++ b/websockets/Secure-Close-undefined.any.js
@@ -0,0 +1,11 @@
+// META: script=websocket.sub.js
+
+var test = async_test();
+
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+  wsocket.close(undefined);
+  test.done();
+}), true);
diff --git a/websockets/Secure-Close-undefined.htm b/websockets/Secure-Close-undefined.htm
deleted file mode 100644
index 9bd4861..0000000
--- a/websockets/Secure-Close-undefined.htm
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Close Secure WebSocket - Code is undefined</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var test = async_test();
-
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', test.step_func(function (evt) {
-            wsocket.close(undefined);
-            test.done();
-        }), true);
-    </script>
-
-</body>
-</html>
diff --git a/websockets/Secure-Send-65K-data.any.js b/websockets/Secure-Send-65K-data.any.js
new file mode 100644
index 0000000..daa937a
--- /dev/null
+++ b/websockets/Secure-Send-65K-data.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send  65K data on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send 65K data on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send 65K data on a Secure WebSocket - Connection should be closed");
+
+var data = "";
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  for (var i = 0; i < 65000; i++) {
+    data = data + "c";
+  }
+  wsocket.send(data);
+  assert_equals(data.length, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-65K-data.htm b/websockets/Secure-Send-65K-data.htm
deleted file mode 100644
index f9bbe37..0000000
--- a/websockets/Secure-Send-65K-data.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send 65K data - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send  65K data on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send 65K data on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send 65K data on a Secure WebSocket - Connection should be closed");
-
-        var data = "";
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            for (var i = 0; i < 65000; i++) {
-                data = data + "c";
-            }
-            wsocket.send(data);
-            assert_equals(data.length, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-65K-arraybuffer.any.js b/websockets/Secure-Send-binary-65K-arraybuffer.any.js
new file mode 100644
index 0000000..17859e5
--- /dev/null
+++ b/websockets/Secure-Send-binary-65K-arraybuffer.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  wsocket.send(data);
+  assert_equals(datasize, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data.byteLength, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-65K-arraybuffer.htm b/websockets/Secure-Send-binary-65K-arraybuffer.htm
deleted file mode 100644
index 64cfc02..0000000
--- a/websockets/Secure-Send-binary-65K-arraybuffer.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send 65K binary data - ArrayBuffer - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send 65K binary data on a Secure WebSocket - ArrayBuffer - Connection should be closed");
-
-        var data = "";
-        var datasize = 65000;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            wsocket.send(data);
-            assert_equals(datasize, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data.byteLength, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybuffer.any.js b/websockets/Secure-Send-binary-arraybuffer.any.js
new file mode 100644
index 0000000..6e4c08d
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybuffer.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 15;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  wsocket.send(data);
+  assert_equals(datasize, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data.byteLength, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybuffer.htm b/websockets/Secure-Send-binary-arraybuffer.htm
deleted file mode 100644
index 0e068ff..0000000
--- a/websockets/Secure-Send-binary-arraybuffer.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBuffer - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - ArrayBuffer - Connection should be closed");
-
-        var data = "";
-        var datasize = 15;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            wsocket.send(data);
-            assert_equals(datasize, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data.byteLength, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-float32.any.js b/websockets/Secure-Send-binary-arraybufferview-float32.any.js
new file mode 100644
index 0000000..9825d34
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-float32.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Float32Array(data);
+  for (var i = 0; i < 2; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Float32Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-float32.htm b/websockets/Secure-Send-binary-arraybufferview-float32.htm
deleted file mode 100644
index 30a8010..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-float32.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Float32Array - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Float32Array(data);
-            for(var i = 0; i < 2; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Float32Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-float64.any.js b/websockets/Secure-Send-binary-arraybufferview-float64.any.js
new file mode 100644
index 0000000..4dcac40
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-float64.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Float64Array(data);
+  for (var i = 0; i < 1; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Float64Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-float64.htm b/websockets/Secure-Send-binary-arraybufferview-float64.htm
deleted file mode 100644
index 1e121a8..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-float64.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Float32Array - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Float64Array(data);
-            for (var i = 0; i < 1; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Float64Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-int32.any.js b/websockets/Secure-Send-binary-arraybufferview-int32.any.js
new file mode 100644
index 0000000..655af21
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-int32.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Int32Array(data);
+  for (var i = 0; i < 2; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Int32Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-int32.htm b/websockets/Secure-Send-binary-arraybufferview-int32.htm
deleted file mode 100644
index 316c62c..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-int32.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Int32Array - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Int32Array(data);
-            for(var i = 0; i < 2; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Int32Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.any.js b/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.any.js
new file mode 100644
index 0000000..16f050f
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Uint16Array(data, 2, 2);
+  for (var i = 0; i < 4; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Uint16Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.htm b/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.htm
deleted file mode 100644
index 1737d93..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-uint16-offset-length.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Uint16Array with offset and length - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Uint16Array(data, 2, 2);
-            for(var i = 0; i < 4; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Uint16Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint32-offset.any.js b/websockets/Secure-Send-binary-arraybufferview-uint32-offset.any.js
new file mode 100644
index 0000000..8976b3d
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-uint32-offset.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Uint32Array(data, 0);
+  for (var i = 0; i < 2; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Uint32Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint32-offset.htm b/websockets/Secure-Send-binary-arraybufferview-uint32-offset.htm
deleted file mode 100644
index 735bae5..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-uint32-offset.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Uint32Array with offset - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Uint32Array(data, 0);
-            for(var i = 0; i < 2; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Uint32Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.any.js b/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.any.js
new file mode 100644
index 0000000..9e9d1b5
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Uint8Array(data, 2, 4);
+  for (var i = 0; i < 8; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Uint8Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.htm b/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.htm
deleted file mode 100644
index 83e1435..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-uint8-offset-length.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Uint8Array with offset and length - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Uint8Array(data, 2, 4);
-            for(var i = 0; i < 8; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Uint8Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint8-offset.any.js b/websockets/Secure-Send-binary-arraybufferview-uint8-offset.any.js
new file mode 100644
index 0000000..f563cec
--- /dev/null
+++ b/websockets/Secure-Send-binary-arraybufferview-uint8-offset.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Uint8Array(data, 2);
+  for (var i = 0; i < 8; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Uint8Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-arraybufferview-uint8-offset.htm b/websockets/Secure-Send-binary-arraybufferview-uint8-offset.htm
deleted file mode 100644
index 413ec34..0000000
--- a/websockets/Secure-Send-binary-arraybufferview-uint8-offset.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Uint8Array with offset - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-    var view;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-        view = new Uint8Array(data, 2);
-        for(var i = 0; i < 8; i++) {
-        view[i] = i;
-        }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Uint8Array(evt.data);
-        for(var i = 0; i < resultView.length; i++) {
-            assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-        }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-binary-blob.any.js b/websockets/Secure-Send-binary-blob.any.js
new file mode 100644
index 0000000..8bf0f12
--- /dev/null
+++ b/websockets/Secure-Send-binary-blob.any.js
@@ -0,0 +1,34 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var isOpenCalled = false;
+
+var wsocket = CreateWebSocket(true, false, false);
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "blob";
+  for (var i = 0; i < datasize; i++)
+    data += String.fromCharCode(0);
+  data = new Blob([data]);
+  isOpenCalled = true;
+  wsocket.send(data);
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_true(evt.data instanceof Blob);
+  assert_equals(evt.data.size, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_true(evt.wasClean, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-binary-blob.htm b/websockets/Secure-Send-binary-blob.htm
deleted file mode 100644
index 6d52aa7..0000000
--- a/websockets/Secure-Send-binary-blob.htm
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - Blob - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a Secure WebSocket - Blob - Connection should be closed");
-
-        var data = "";
-        var datasize = 65000;
-        var isOpenCalled = false;
-
-        var wsocket = CreateWebSocket(true, false, false);
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "blob";
-            for (var i = 0; i < datasize; i++)
-                data += String.fromCharCode(0);
-            data = new Blob([data]);
-            isOpenCalled = true;
-            wsocket.send(data);
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_true(evt.data instanceof Blob);
-            assert_equals(evt.data.size, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_true(evt.wasClean, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-data.any.js b/websockets/Secure-Send-data.any.js
new file mode 100644
index 0000000..04c720d
--- /dev/null
+++ b/websockets/Secure-Send-data.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send  data on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send data on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send data on a Secure WebSocket - Connection should be closed");
+
+var data = "Message to send";
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-data.htm b/websockets/Secure-Send-data.htm
deleted file mode 100644
index d7c1595..0000000
--- a/websockets/Secure-Send-data.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send data - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send  data on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send data on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send data on a Secure WebSocket - Connection should be closed");
-
-        var data = "Message to send";
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-null.any.js b/websockets/Secure-Send-null.any.js
new file mode 100644
index 0000000..7c374c2
--- /dev/null
+++ b/websockets/Secure-Send-null.any.js
@@ -0,0 +1,30 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Connection should be closed");
+
+var data = null;
+var nullReturned = false;
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  if ("null" == evt.data || "" == evt.data)
+    nullReturned = true;
+  assert_true(nullReturned);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-null.htm b/websockets/Secure-Send-null.htm
deleted file mode 100644
index 0a20335..0000000
--- a/websockets/Secure-Send-null.htm
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send null data - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send null data on a Secure WebSocket - Connection should be closed");
-
-        var data = null;
-        var nullReturned = false;
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            if ("null" == evt.data || "" == evt.data)
-                nullReturned = true;
-            assert_true(nullReturned);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-paired-surrogates.any.js b/websockets/Secure-Send-paired-surrogates.any.js
new file mode 100644
index 0000000..073f064
--- /dev/null
+++ b/websockets/Secure-Send-paired-surrogates.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Connection should be closed");
+
+var data = "\uD801\uDC07";
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length * 2, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-paired-surrogates.htm b/websockets/Secure-Send-paired-surrogates.htm
deleted file mode 100644
index 959e307..0000000
--- a/websockets/Secure-Send-paired-surrogates.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send (paired surrogates) data - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send paired surrogates data on a Secure WebSocket - Connection should be closed");
-
-        var data = "\uD801\uDC07";
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length * 2, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-unicode-data.any.js b/websockets/Secure-Send-unicode-data.any.js
new file mode 100644
index 0000000..a3518c1
--- /dev/null
+++ b/websockets/Secure-Send-unicode-data.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send  unicode data on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send unicode data on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send unicode data on a Secure WebSocket - Connection should be closed");
+
+var data = "¥¥¥¥¥¥";
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length * 2, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-unicode-data.htm b/websockets/Secure-Send-unicode-data.htm
deleted file mode 100644
index 42de641..0000000
--- a/websockets/Secure-Send-unicode-data.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send unicode data - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send  unicode data on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send unicode data on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send unicode data on a Secure WebSocket - Connection should be closed");
-
-        var data = "¥¥¥¥¥¥";
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length * 2, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Secure-Send-unpaired-surrogates.any.js b/websockets/Secure-Send-unpaired-surrogates.any.js
new file mode 100644
index 0000000..83f3e7b
--- /dev/null
+++ b/websockets/Secure-Send-unpaired-surrogates.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Connection should be closed");
+
+var data = "\uD807";
+var replacementChar = "\uFFFD";
+var wsocket = CreateWebSocket(true, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, replacementChar);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Secure-Send-unpaired-surrogates.htm b/websockets/Secure-Send-unpaired-surrogates.htm
deleted file mode 100644
index 11df504..0000000
--- a/websockets/Secure-Send-unpaired-surrogates.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send unpaired surrogates - Secure WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send unpaired surrogates on a Secure WebSocket - Connection should be closed");
-
-        var data = "\uD807";
-        var replacementChar = "\uFFFD";
-        var wsocket = CreateWebSocket(true, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, replacementChar);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-0byte-data.any.js b/websockets/Send-0byte-data.any.js
new file mode 100644
index 0000000..131a19d
--- /dev/null
+++ b/websockets/Send-0byte-data.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Connection should be closed");
+
+var data = "";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-0byte-data.htm b/websockets/Send-0byte-data.htm
deleted file mode 100644
index 52ab7ca..0000000
--- a/websockets/Send-0byte-data.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send 0 byte data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send 0 byte data on a WebSocket - Connection should be closed");
-
-        var data = "";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-65K-data.any.js b/websockets/Send-65K-data.any.js
new file mode 100644
index 0000000..172e6ee
--- /dev/null
+++ b/websockets/Send-65K-data.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send 65K data on a WebSocket -  Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send 65K data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send 65K data on a WebSocket - Connection should be closed");
+
+var data = "";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  for (var i = 0; i < 65000; i++) {
+    data = data + "c";
+  }
+  wsocket.send(data);
+  assert_equals(data.length, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-65K-data.htm b/websockets/Send-65K-data.htm
deleted file mode 100644
index 7d22340..0000000
--- a/websockets/Send-65K-data.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send 65K data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send 65K data on a WebSocket -  Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send 65K data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send 65K data on a WebSocket - Connection should be closed");
-
-        var data = "";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            for (var i = 0; i < 65000; i++) {
-                data = data + "c";
-            }
-            wsocket.send(data);
-            assert_equals(data.length, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-Unpaired-Surrogates.any.js b/websockets/Send-Unpaired-Surrogates.any.js
new file mode 100644
index 0000000..65bb2b1
--- /dev/null
+++ b/websockets/Send-Unpaired-Surrogates.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Connection should be closed");
+
+var data = "\uD807";
+var replacementChar = "\uFFFD";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, replacementChar);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-Unpaired-Surrogates.htm b/websockets/Send-Unpaired-Surrogates.htm
deleted file mode 100644
index 6697e68..0000000
--- a/websockets/Send-Unpaired-Surrogates.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send unpaired surrogates - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send unpaired surrogates on a WebSocket - Connection should be closed");
-
-        var data = "\uD807";
-        var replacementChar = "\uFFFD";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, replacementChar);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-before-open.any.js b/websockets/Send-before-open.any.js
new file mode 100644
index 0000000..101a1a2
--- /dev/null
+++ b/websockets/Send-before-open.any.js
@@ -0,0 +1,8 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var wsocket = CreateWebSocket(false, false, false);
+  assert_throws("INVALID_STATE_ERR", function() {
+    wsocket.send("Message to send")
+  });
+}, "W3C WebSocket API - Send data on a WebSocket before connection is opened - INVALID_STATE_ERR is returned")
diff --git a/websockets/Send-before-open.htm b/websockets/Send-before-open.htm
deleted file mode 100644
index 1aace54..0000000
--- a/websockets/Send-before-open.htm
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send WebSocket - Send before connection in established</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-        test(function () {
-            var wsocket = CreateWebSocket(false, false, false);
-            assert_throws("INVALID_STATE_ERR", function () { wsocket.send("Message to send") });
-        }, "W3C WebSocket API - Send data on a WebSocket before connection is opened - INVALID_STATE_ERR is returned")
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-binary-65K-arraybuffer.any.js b/websockets/Send-binary-65K-arraybuffer.any.js
new file mode 100644
index 0000000..f446a25
--- /dev/null
+++ b/websockets/Send-binary-65K-arraybuffer.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  wsocket.send(data);
+  assert_equals(datasize, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data.byteLength, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-binary-65K-arraybuffer.htm b/websockets/Send-binary-65K-arraybuffer.htm
deleted file mode 100644
index 95c12cf..0000000
--- a/websockets/Send-binary-65K-arraybuffer.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send 65K binary data - ArrayBuffer - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed");
-
-        var data = "";
-        var datasize = 65000;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            wsocket.send(data);
-            assert_equals(datasize, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data.byteLength, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-binary-arraybuffer.any.js b/websockets/Send-binary-arraybuffer.any.js
new file mode 100644
index 0000000..6205143
--- /dev/null
+++ b/websockets/Send-binary-arraybuffer.any.js
@@ -0,0 +1,31 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 15;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  wsocket.send(data);
+  assert_equals(datasize, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data.byteLength, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-binary-arraybuffer.htm b/websockets/Send-binary-arraybuffer.htm
deleted file mode 100644
index b960b92..0000000
--- a/websockets/Send-binary-arraybuffer.htm
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBuffer - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBuffer - Connection should be closed");
-
-        var data = "";
-        var datasize = 15;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            wsocket.send(data);
-            assert_equals(datasize, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data.byteLength, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-binary-arraybufferview-int16-offset.any.js b/websockets/Send-binary-arraybufferview-int16-offset.any.js
new file mode 100644
index 0000000..7022668
--- /dev/null
+++ b/websockets/Send-binary-arraybufferview-int16-offset.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  view = new Int16Array(data, 2);
+  for (var i = 0; i < 4; i++) {
+    view[i] = i;
+  }
+  wsocket.send(view);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Int16Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-binary-arraybufferview-int16-offset.htm b/websockets/Send-binary-arraybufferview-int16-offset.htm
deleted file mode 100644
index 0ebd3ac..0000000
--- a/websockets/Send-binary-arraybufferview-int16-offset.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Int16Array with offset - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var view;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            view = new Int16Array(data, 2);
-            for(var i = 0; i < 4; i++) {
-                view[i] = i;
-            }
-            wsocket.send(view);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Int16Array(evt.data);
-            for(var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-binary-arraybufferview-int8.any.js b/websockets/Send-binary-arraybufferview-int8.any.js
new file mode 100644
index 0000000..242c8c6
--- /dev/null
+++ b/websockets/Send-binary-arraybufferview-int8.any.js
@@ -0,0 +1,38 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var int8View;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "arraybuffer";
+  data = new ArrayBuffer(datasize);
+  int8View = new Int8Array(data);
+  for (var i = 0; i < 8; i++) {
+    int8View[i] = i;
+  }
+  wsocket.send(int8View);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  var resultView = new Int8Array(evt.data);
+  for (var i = 0; i < resultView.length; i++) {
+    assert_equals(resultView[i], int8View[i], "ArrayBufferView returned is the same");
+  }
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-binary-arraybufferview-int8.htm b/websockets/Send-binary-arraybufferview-int8.htm
deleted file mode 100644
index 5336a87..0000000
--- a/websockets/Send-binary-arraybufferview-int8.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - ArrayBufferView - Int8Array - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be closed");
-
-        var data = "";
-        var datasize = 8;
-        var int8View;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "arraybuffer";
-            data = new ArrayBuffer(datasize);
-            int8View = new Int8Array(data);
-            for (var i = 0; i < 8; i++) {
-                int8View[i] = i;
-            }
-            wsocket.send(int8View);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            var resultView = new Int8Array(evt.data);
-            for (var i = 0; i < resultView.length; i++) {
-                assert_equals(resultView[i], int8View[i], "ArrayBufferView returned is the same");
-            }
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-binary-blob.any.js b/websockets/Send-binary-blob.any.js
new file mode 100644
index 0000000..ee6486e
--- /dev/null
+++ b/websockets/Send-binary-blob.any.js
@@ -0,0 +1,34 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var isOpenCalled = false;
+
+var wsocket = CreateWebSocket(false, false, false);
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.binaryType = "blob";
+  for (var i = 0; i < datasize; i++)
+    data += String.fromCharCode(0);
+  data = new Blob([data]);
+  isOpenCalled = true;
+  wsocket.send(data);
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_true(evt.data instanceof Blob);
+  assert_equals(evt.data.size, datasize);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_true(evt.wasClean, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-binary-blob.htm b/websockets/Send-binary-blob.htm
deleted file mode 100644
index fa14f4c..0000000
--- a/websockets/Send-binary-blob.htm
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send binary data - Blob - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send binary data on a WebSocket - Blob - Connection should be closed");
-
-        var data = "";
-        var datasize = 65000;
-        var isOpenCalled = false;
-
-        var wsocket = CreateWebSocket(false, false, false);
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.binaryType = "blob";
-            for (var i = 0; i < datasize; i++)
-                data += String.fromCharCode(0);
-            data = new Blob([data]);
-            isOpenCalled = true;
-            wsocket.send(data);
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_true(evt.data instanceof Blob);
-            assert_equals(evt.data.size, datasize);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_true(evt.wasClean, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-data.any.js b/websockets/Send-data.any.js
new file mode 100644
index 0000000..487393b
--- /dev/null
+++ b/websockets/Send-data.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send data on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send data on a WebSocket - Connection should be closed");
+
+var data = "Message to send";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-data.htm b/websockets/Send-data.htm
deleted file mode 100644
index 547a15f..0000000
--- a/websockets/Send-data.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send data on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send data on a WebSocket - Connection should be closed");
-
-        var data = "Message to send";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-null.any.js b/websockets/Send-null.any.js
new file mode 100644
index 0000000..8d8d6b7
--- /dev/null
+++ b/websockets/Send-null.any.js
@@ -0,0 +1,30 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send null data on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send null data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send null data on a WebSocket - Connection should be closed");
+
+var data = null;
+var nullReturned = false;
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  if ("null" == evt.data || "" == evt.data)
+    nullReturned = true;
+  assert_true(nullReturned);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-null.htm b/websockets/Send-null.htm
deleted file mode 100644
index 5b7a6f1..0000000
--- a/websockets/Send-null.htm
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send null data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send null data on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send null data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send null data on a WebSocket - Connection should be closed");
-
-        var data = null;
-        var nullReturned = false;
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            if ("null" == evt.data || "" == evt.data)
-                nullReturned = true;
-            assert_true(nullReturned);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-paired-surrogates.any.js b/websockets/Send-paired-surrogates.any.js
new file mode 100644
index 0000000..f12b001
--- /dev/null
+++ b/websockets/Send-paired-surrogates.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Connection should be closed");
+
+var data = "\uD801\uDC07";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length * 2, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-paired-surrogates.htm b/websockets/Send-paired-surrogates.htm
deleted file mode 100644
index 4413221..0000000
--- a/websockets/Send-paired-surrogates.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send (paired surrogates) data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send (paired surrogates) data on a WebSocket - Connection should be closed");
-
-        var data = "\uD801\uDC07";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length * 2, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/Send-unicode-data.any.js b/websockets/Send-unicode-data.any.js
new file mode 100644
index 0000000..ce2b9a0
--- /dev/null
+++ b/websockets/Send-unicode-data.any.js
@@ -0,0 +1,28 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Connection should be opened");
+var testMessage = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Message should be received");
+var testClose = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Connection should be closed");
+
+var data = "¥¥¥¥¥¥";
+var wsocket = CreateWebSocket(false, false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  wsocket.send(data);
+  assert_equals(data.length * 2, wsocket.bufferedAmount);
+  isOpenCalled = true;
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('message', testMessage.step_func(function(evt) {
+  assert_equals(evt.data, data);
+  wsocket.close();
+  testMessage.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(isOpenCalled, "WebSocket connection should be open");
+  assert_equals(evt.wasClean, true, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/Send-unicode-data.htm b/websockets/Send-unicode-data.htm
deleted file mode 100644
index aba7918..0000000
--- a/websockets/Send-unicode-data.htm
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Send unicode data - WebSocket</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Connection should be opened");
-        var testMessage = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Message should be received");
-        var testClose = async_test("W3C WebSocket API - Send unicode data on a WebSocket - Connection should be closed");
-
-        var data = "¥¥¥¥¥¥";
-        var wsocket = CreateWebSocket(false, false, false);
-        var isOpenCalled = false;
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            wsocket.send(data);
-            assert_equals(data.length * 2, wsocket.bufferedAmount);
-            isOpenCalled = true;
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('message', testMessage.step_func(function (evt) {
-            assert_equals(evt.data, data);
-            wsocket.close();
-            testMessage.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(isOpenCalled, "WebSocket connection should be open");
-            assert_equals(evt.wasClean, true, "wasClean should be true");
-            testClose.done();
-        }), true);
-    </script>
-</body>
-</html>
diff --git a/websockets/binaryType-wrong-value.any.js b/websockets/binaryType-wrong-value.any.js
new file mode 100644
index 0000000..6030608
--- /dev/null
+++ b/websockets/binaryType-wrong-value.any.js
@@ -0,0 +1,19 @@
+// META: script=websocket.sub.js
+
+var testOpen = async_test("W3C WebSocket API - Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be opened");
+var testClose = async_test("W3C WebSocket API - Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false, false);
+
+wsocket.addEventListener('open', testOpen.step_func(function(evt) {
+  assert_equals(wsocket.binaryType, "blob");
+  wsocket.binaryType = "notBlobOrArrayBuffer";
+  assert_equals(wsocket.binaryType, "blob");
+  wsocket.close();
+  testOpen.done();
+}), true);
+
+wsocket.addEventListener('close', testClose.step_func(function(evt) {
+  assert_true(evt.wasClean, "wasClean should be true");
+  testClose.done();
+}), true);
diff --git a/websockets/binaryType-wrong-value.htm b/websockets/binaryType-wrong-value.htm
deleted file mode 100644
index 2bab671..0000000
--- a/websockets/binaryType-wrong-value.htm
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>W3C WebSocket API - Create Secure WebSocket - binaryType is set to something other than blob/arraybuffer</title>
-    <script type="text/javascript" src="/resources/testharness.js"></script>
-    <script type="text/javascript" src="/resources/testharnessreport.js"></script>
-    <script type="text/javascript" src="websocket.sub.js"></script>
-</head>
-<body>
-    <div id="log"></div>
-    <script type="text/javascript">
-
-        var testOpen = async_test("W3C WebSocket API - Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be opened");
-        var testClose = async_test("W3C WebSocket API - Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be closed");
-
-        var wsocket = CreateWebSocket(true, false, false);
-
-        wsocket.addEventListener('open', testOpen.step_func(function (evt) {
-            assert_equals(wsocket.binaryType, "blob");
-            wsocket.binaryType = "notBlobOrArrayBuffer";
-            assert_equals(wsocket.binaryType, "blob");
-            wsocket.close();
-            testOpen.done();
-        }), true);
-
-        wsocket.addEventListener('close', testClose.step_func(function (evt) {
-            assert_true(evt.wasClean, "wasClean should be true");
-            testClose.done();
-        }), true);
-
-    </script>
-
-</body>
-</html>
diff --git a/websockets/close-invalid.any.js b/websockets/close-invalid.any.js
index a930495..8ac1da3 100644
--- a/websockets/close-invalid.any.js
+++ b/websockets/close-invalid.any.js
@@ -1,20 +1,20 @@
 // META: script=websocket.sub.js
 
 [
-    [0, "0"],
-    [500, "500"],
-    [NaN, "NaN"],
-    ["string", "String"],
-    [null, "null"],
-    [0x10000 + 1000, "2**16+1000"],
+  [0, "0"],
+  [500, "500"],
+  [NaN, "NaN"],
+  ["string", "String"],
+  [null, "null"],
+  [0x10000 + 1000, "2**16+1000"],
 ].forEach(function(t) {
-    [true, false].forEach(function(secure) {
-        test(function() {
-            var ws = CreateWebSocket(secure, false, false);
-            assert_throws("InvalidAccessError", function () {
-                ws.close(t[0]);
-            });
-            wsocket.onerror = this.unreached_func();
-        }, t[1] + " on a " + (secure ? "secure" : "insecure") + " websocket");
-    });
+  [true, false].forEach(function(secure) {
+    test(function() {
+      var ws = CreateWebSocket(secure, false, false);
+      assert_throws("InvalidAccessError", function() {
+        ws.close(t[0]);
+      });
+      wsocket.onerror = this.unreached_func();
+    }, t[1] + " on a " + (secure ? "secure" : "insecure") + " websocket");
+  });
 });
diff --git a/websockets/constructor.any.js b/websockets/constructor.any.js
new file mode 100644
index 0000000..0605d5e
--- /dev/null
+++ b/websockets/constructor.any.js
@@ -0,0 +1,7 @@
+// META: script=websocket.sub.js
+
+test(function() {
+  var ws = new WebSocket("ws://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH,
+    "echo", "Stray argument")
+  assert_true(ws instanceof WebSocket, "Expected a WebSocket instance.")
+}, "Calling the WebSocket constructor with too many arguments should not throw.")
diff --git a/websockets/constructor.html b/websockets/constructor.html
deleted file mode 100644
index c135b32..0000000
--- a/websockets/constructor.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE html>
-<title>WebSocket constructor</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="websocket.sub.js"></script>
-<div id="log"></div>
-<script>
-test(function() {
-  var ws = new WebSocket("ws://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH,
-                         "echo", "Stray argument")
-  assert_true(ws instanceof WebSocket, "Expected a WebSocket instance.")
-}, "Calling the WebSocket constructor with too many arguments should not throw.")
-</script>
diff --git a/websockets/eventhandlers.any.js b/websockets/eventhandlers.any.js
new file mode 100644
index 0000000..b30b0b6
--- /dev/null
+++ b/websockets/eventhandlers.any.js
@@ -0,0 +1,13 @@
+// META: script=websocket.sub.js
+
+function testEventHandler(name) {
+  test(function() {
+    var ws = new WebSocket("ws://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH,
+      "echo")
+    assert_equals(ws["on" + name], null);
+    ws["on" + name] = function() {};
+    ws["on" + name] = 2;
+    assert_equals(ws["on" + name], null);
+  }, "Event handler for " + name + " should have [TreatNonCallableAsNull]")
+}
+["open", "error", "close", "message"].forEach(testEventHandler);
diff --git a/websockets/eventhandlers.html b/websockets/eventhandlers.html
deleted file mode 100644
index 95aeb0b..0000000
--- a/websockets/eventhandlers.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<title>WebSocket event handlers</title>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="websocket.sub.js"></script>
-<div id="log"></div>
-<script>
-function testEventHandler(name) {
-  test(function() {
-    var ws = new WebSocket("ws://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH,
-                           "echo")
-    assert_equals(ws["on" + name], null);
-    ws["on" + name] = function() {};
-    ws["on" + name] = 2;
-    assert_equals(ws["on" + name], null);
-  }, "Event handler for " + name + " should have [TreatNonCallableAsNull]")
-}
-["open", "error", "close", "message"].forEach(testEventHandler);
-</script>
diff --git a/x-frame-options/redirect.sub.html b/x-frame-options/redirect.sub.html
new file mode 100644
index 0000000..0bc708b
--- /dev/null
+++ b/x-frame-options/redirect.sub.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./support/helper.js"></script>
+<body>
+<script>
+  async_test(t => {
+    var i = document.createElement('iframe');
+    i.src = "./support/redirect.py?value=DENY&url=/x-frame-options/support/xfo.py%3Fvalue%3DALLOWALL";
+
+    wait_for_message_from(i, t)
+      .then(t.step_func_done(e => {
+        assert_equals(e.data, "Loaded");
+        i.remove();
+      }));
+
+    document.body.appendChild(i);
+  }, "XFO on redirect responses is ignored.");
+</script>
diff --git a/x-frame-options/support/redirect.py b/x-frame-options/support/redirect.py
new file mode 100644
index 0000000..0addf20
--- /dev/null
+++ b/x-frame-options/support/redirect.py
@@ -0,0 +1,4 @@
+def main(request, response):
+    response.status = 302
+    response.headers.set("X-Frame-Options", request.GET.first("value"))
+    response.headers.set("Location", request.GET.first("url"))
diff --git a/xhr/abort-during-open.any.js b/xhr/abort-during-open.any.js
new file mode 100644
index 0000000..42a1bce
--- /dev/null
+++ b/xhr/abort-during-open.any.js
@@ -0,0 +1,18 @@
+var test = async_test("XMLHttpRequest: abort() during OPEN");
+test.step(function() {
+  var client = new XMLHttpRequest()
+  client.open("GET", "...")
+  client.onreadystatechange = function() {
+    test.step(function() {
+      assert_unreached()
+    })
+  }
+  assert_equals(client.readyState, 1, "before abort()")
+  assert_equals(client.status, 0)
+  assert_equals(client.statusText, "")
+  client.abort()
+  assert_equals(client.readyState, 1, "after abort()")
+  assert_equals(client.status, 0)
+  assert_equals(client.statusText, "")
+})
+test.done()
diff --git a/xhr/abort-during-open.htm b/xhr/abort-during-open.htm
deleted file mode 100644
index dde94f2..0000000
--- a/xhr/abort-during-open.htm
+++ /dev/null
@@ -1,14 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <title>XMLHttpRequest: abort() during OPEN</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-abort()-method" data-tested-assertations="following-sibling::ol/li[4] following-sibling::ol/li[5]" />
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following-sibling::ol/li[1]" />
-  </head>
-  <body>
-    <div id="log"></div>
-    <script src="abort-during-open.js"></script>
-  </body>
-</html>
diff --git a/xhr/abort-during-open.js b/xhr/abort-during-open.js
deleted file mode 100644
index 26d3f57..0000000
--- a/xhr/abort-during-open.js
+++ /dev/null
@@ -1,18 +0,0 @@
-var test = async_test()
-test.step(function() {
-  var client = new XMLHttpRequest()
-  client.open("GET", "...")
-  client.onreadystatechange = function() {
-    test.step(function() {
-      assert_unreached()
-    })
-  }
-  assert_equals(client.readyState, 1, "before abort()")
-  assert_equals(client.status, 0)
-  assert_equals(client.statusText, "")
-  client.abort()
-  assert_equals(client.readyState, 1, "after abort()")
-  assert_equals(client.status, 0)
-  assert_equals(client.statusText, "")
-})
-test.done()
diff --git a/xhr/abort-during-open.worker.js b/xhr/abort-during-open.worker.js
deleted file mode 100644
index ffb687d..0000000
--- a/xhr/abort-during-open.worker.js
+++ /dev/null
@@ -1,3 +0,0 @@
-importScripts("/resources/testharness.js");
-importScripts("abort-during-open.js");
-done();
diff --git a/xhr/close-worker-with-xhr-in-progress.html b/xhr/close-worker-with-xhr-in-progress.html
new file mode 100644
index 0000000..4d03bea
--- /dev/null
+++ b/xhr/close-worker-with-xhr-in-progress.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+  function workerCode(origin) {
+    const xhr = new XMLHttpRequest();
+    xhr.open('GET', origin + '/xhr/resources/image.gif?pipe=trickle(100:d2)', true);
+    xhr.responseType = 'blob';
+    xhr.send();
+    postMessage('sent');
+  }
+
+  const workerBlob = new Blob([workerCode.toString() + ";workerCode('" + location.origin + "');"], {type:"application/javascript"});
+  const w = new Worker(URL.createObjectURL(workerBlob));
+  w.onmessage = t.step_func(e => {
+    assert_equals(e.data, 'sent');
+    t.step_timeout(t.step_func(() => {
+      w.terminate();
+      t.step_timeout(t.step_func_done(() => {}), 500);
+    }, 100));
+  });
+}, 'Terminating a worker with a XHR in progress doesn\'t crash');
+</script>
diff --git a/xhr/firing-events-http-content-length.html b/xhr/firing-events-http-content-length.html
index 6e54852..4748ce3 100644
--- a/xhr/firing-events-http-content-length.html
+++ b/xhr/firing-events-http-content-length.html
@@ -4,35 +4,29 @@
     <title>ProgressEvent: firing events for HTTP with Content-Length</title>
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface">
   </head>
   <body>
     <div id="log"></div>
     <script>
-      var test = async_test();
+      async_test(t => {
+        const xhr = new XMLHttpRequest();
+        let progressHappened = false;
 
-      test.step(function() {
-        var xhr = new XMLHttpRequest();
+        xhr.onprogress = t.step_func(pe => {
+          assert_equals(pe.type, "progress");
+          assert_greater_than_equal(pe.loaded, 0, "loaded");
+          assert_true(pe.lengthComputable, "lengthComputable");
+          assert_equals(pe.total, 1300, "total");
+          progressHappened = true;
+        });
 
-        xhr.onprogress = function(pe) {
-          test.step(function() {
-            if(pe.type == "progress") {
-              assert_greater_than_equal(pe.loaded, 0, "loaded");
-              assert_true(pe.lengthComputable, "lengthComputable");
-              assert_equals(pe.total, 1300, "total");
-            }
-          }, "Check lengthComputed, loaded, total when Content-Length is given.");
-        }
+        xhr.onloadend = t.step_func_done(() => {
+          assert_true(progressHappened);
+        });
 
-        // "loadstart", "error", "abort", "load" tests are out of scope.
-        // They SHOULD be tested in each spec that implement ProgressEvent.
-
-        xhr.onloadend = function(pe) {
-          test.done();
-        }
         xhr.open("GET", "resources/trickle.py?ms=0&count=100&specifylength=1", true);
         xhr.send(null);
-      })
+      });
     </script>
   </body>
 </html>
diff --git a/xhr/firing-events-http-no-content-length.html b/xhr/firing-events-http-no-content-length.html
index 2a4614a..ddf7dd8 100644
--- a/xhr/firing-events-http-no-content-length.html
+++ b/xhr/firing-events-http-no-content-length.html
@@ -4,35 +4,32 @@
     <title>ProgressEvent: firing events for HTTP with no Content-Length</title>
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface">
   </head>
   <body>
     <div id="log"></div>
     <script>
-      var test = async_test();
+      async_test(t => {
+        const xhr = new XMLHttpRequest();
+        let progressHappened = false;
 
-      test.step(function() {
-        var xhr = new XMLHttpRequest();
-
-        xhr.onprogress = function(pe) {
-          test.step(function() {
-            if(pe.type == "progress") {
-              assert_greater_than_equal(pe.loaded, 0, "loaded");
-              assert_false(pe.lengthComputable, "lengthComputable");
-              assert_equals(pe.total, 0, "total");
-            }
-          }, "Check lengthComputed, loaded, total when Content-Length is NOT given.");
-        }
+        xhr.onprogress = t.step_func(pe => {
+          assert_equals(pe.type, "progress");
+          assert_greater_than_equal(pe.loaded, 0, "loaded");
+          assert_false(pe.lengthComputable, "lengthComputable");
+          assert_equals(pe.total, 0, "total");
+          progressHappened = true;
+        });
 
         // "loadstart", "error", "abort", "load" tests are out of scope.
         // They SHOULD be tested in each spec that implement ProgressEvent.
 
-        xhr.onloadend = function(pe) {
-          test.done();
-        }
+        xhr.onloadend = t.step_func_done(() => {
+          assert_true(progressHappened);
+        });
+
         xhr.open("GET", "resources/trickle.py?ms=0&count=100", true);
         xhr.send(null);
-      })
+      });
     </script>
   </body>
 </html>
diff --git a/xhr/overridemimetype-blob.html b/xhr/overridemimetype-blob.html
index 83378a8..db4b880 100644
--- a/xhr/overridemimetype-blob.html
+++ b/xhr/overridemimetype-blob.html
@@ -6,60 +6,51 @@
 <div id="log"></div>
 <script>
 async_test(t => {
-  const client = new XMLHttpRequest()
+  const client = new XMLHttpRequest();
   client.onload = t.step_func_done(() => {
-    assert_equals(client.getResponseHeader("Content-Type"), "")
-    assert_equals(client.response.type, "text/xml")
-  })
-  client.open("GET", "resources/status.py")
-  client.responseType = "blob"
-  client.send()
-}, "Use text/xml as fallback MIME type")
+    assert_equals(client.getResponseHeader("Content-Type"), "");
+    assert_equals(client.response.type, "text/xml");
+  });
+  client.open("GET", "resources/status.py");
+  client.responseType = "blob";
+  client.send();
+}, "Use text/xml as fallback MIME type");
 
 async_test(t => {
-  const client = new XMLHttpRequest()
+  const client = new XMLHttpRequest();
   client.onload = t.step_func_done(() => {
-    assert_equals(client.getResponseHeader("Content-Type"), "")
-    assert_equals(client.response.type, "text/xml")
+    assert_equals(client.getResponseHeader("Content-Type"), "");
+    assert_equals(client.response.type, "text/xml");
   })
-  client.open("GET", "resources/status.py?content=thisshouldnotmakeadifferencebutdoes")
-  client.responseType = "blob"
-  client.send()
-}, "Use text/xml as fallback MIME type, 2")
+  client.open("GET", "resources/status.py?content=thisshouldnotmakeadifferencebutdoes");
+  client.responseType = "blob";
+  client.send();
+}, "Use text/xml as fallback MIME type, 2");
 
-async_test(t => {
-  const client = new XMLHttpRequest()
-  client.onload = t.step_func_done(() => {
-    assert_equals(client.getResponseHeader("Content-Type"), "")
-    assert_equals(client.response.type, "application/octet-stream")
-  })
-  client.open("GET", "resources/status.py")
-  client.responseType = "blob"
-  client.overrideMimeType("bogus")
-  client.send()
-}, "Bogus MIME type should end up as application/octet-stream")
+promise_test(() => {
+  // Don't load generated-mime-types.json as sending them all over the network would be prohibitive
+  return fetch("../mimesniff/mime-types/resources/mime-types.json").then(res => res.json()).then(runTests);
+}, "Loading data…");
 
-async_test(t => {
-  const client = new XMLHttpRequest()
-  client.onload = t.step_func_done(() => {
-    assert_equals(client.getResponseHeader("Content-Type"), "")
-    assert_equals(client.response.type, "application/octet-stream")
-  })
-  client.open("GET", "resources/status.py")
-  client.responseType = "blob"
-  client.overrideMimeType("text/xml;charset=†")
-  client.send()
-}, "Bogus MIME type should end up as application/octet-stream, 2")
-
-async_test(t => {
-  const client = new XMLHttpRequest()
-  client.onload = t.step_func_done(() => {
-    assert_equals(client.getResponseHeader("Content-Type"), "")
-    assert_equals(client.response.type, "hi/x")
-  })
-  client.open("GET", "resources/status.py")
-  client.responseType = "blob"
-  client.overrideMimeType("HI/x;test=test")
-  client.send()
-}, "Valid MIME types need to be normalized")
+function runTests(tests) {
+  let index = 0;
+  tests.forEach((val) => {
+    if(typeof val === "string") {
+      return;
+    }
+    index++;
+    async_test(t => {
+      const client = new XMLHttpRequest(),
+            expectedOutput = val.output !== null ? val.output : "application/octet-stream";
+      client.onload = t.step_func_done(() => {
+        assert_equals(client.getResponseHeader("Content-Type"), "");
+        assert_equals(client.response.type, expectedOutput);
+      });
+      client.open("GET", "resources/status.py");
+      client.responseType = "blob";
+      client.overrideMimeType(val.input);
+      client.send();
+    }, index + ") MIME types need to be parsed and serialized: " + val.input);
+  });
+}
 </script>
diff --git a/xhr/overridemimetype-edge-cases.window.js b/xhr/overridemimetype-edge-cases.window.js
new file mode 100644
index 0000000..6dfe755
--- /dev/null
+++ b/xhr/overridemimetype-edge-cases.window.js
@@ -0,0 +1,40 @@
+const testURL = "resources/status.py?type=" + encodeURIComponent("text/plain;charset=windows-1252") + "&content=%C2%F0";
+
+async_test(t => {
+  const client = new XMLHttpRequest();
+  let secondTime = false;
+  client.onload = t.step_func(() => {
+    if(!secondTime) {
+      assert_equals(client.responseText, "\uFFFD");
+      secondTime = true;
+      client.open("GET", testURL);
+      client.send();
+    } else {
+      assert_equals(client.responseText, "Âð");
+      t.done();
+    }
+  });
+  client.open("GET", testURL);
+  client.overrideMimeType("text/plain;charset=UTF-8")
+  client.send();
+}, "overrideMimeType() state needs to be reset across requests");
+
+async_test(t => {
+  const client = new XMLHttpRequest();
+  client.onload = t.step_func_done(() => {
+    assert_equals(client.responseText, "Âð")
+  });
+  client.open("GET", testURL);
+  client.overrideMimeType("text/xml");
+  client.send();
+}, "If charset is not overridden by overrideMimeType() the original continues to be used");
+
+async_test(t => {
+  const client = new XMLHttpRequest();
+  client.onload = t.step_func_done(() => {
+    assert_equals(client.responseText, "\uFFFD")
+  });
+  client.open("GET", testURL);
+  client.overrideMimeType("text/plain;charset=342");
+  client.send();
+}, "Charset can be overridden by overrideMimeType() with a bogus charset");
diff --git a/xhr/resources/authentication.py b/xhr/resources/authentication.py
index 4f65fa2..369a49a 100644
--- a/xhr/resources/authentication.py
+++ b/xhr/resources/authentication.py
@@ -1,9 +1,4 @@
 def main(request, response):
-    if "logout" in request.GET:
-        return ((401, "Unauthorized"),
-                [("WWW-Authenticate", 'Basic realm="test"')],
-                "Logged out, hopefully")
-
     session_user = request.auth.username
     session_pass = request.auth.password
     expected_user_name = request.headers.get("X-User", None)
diff --git a/xhr/responsetext-decoding.htm b/xhr/responsetext-decoding.htm
index c7e3783..fae0104 100644
--- a/xhr/responsetext-decoding.htm
+++ b/xhr/responsetext-decoding.htm
@@ -82,6 +82,7 @@
       request("text/plain", "%EF%BB%BF", "", "text");
       request("text/plain", "%EF%BB%BF%EF%BB%BF", "\uFEFF", "text");
       request("text/plain", "%C2", "\uFFFD", "text");
+      request("text/plain;charset=bogus", "%C2", "\uFFFD", "text");
       request("text/xml", "%FE%FF", "", "text");
       request("text/xml", "%FE%FF%FE%FF", "\uFEFF", "text");
       request("text/xml", "%EF%BB%BF", "", "text");
diff --git a/xhr/send-authentication-competing-names-passwords.htm b/xhr/send-authentication-competing-names-passwords.htm
index d58d9e0..bc6755c 100644
--- a/xhr/send-authentication-competing-names-passwords.htm
+++ b/xhr/send-authentication-competing-names-passwords.htm
@@ -5,50 +5,46 @@
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
     <script src="/common/utils.js"></script>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[9]/ol[1]/li[1] following::ol[1]/li[9]/ol[1]/li[2]" />
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />  </head>
+  </head>
   <body>
     <div id="log"></div>
     <script>
       function request(user1, pass1, user2, pass2, name) {
-        // user1, pass1 will if given become userinfo part of URL
-        // user2, pass2 will if given be passed to open() call
         test(function() {
-          var client = new XMLHttpRequest(),
-              urlstart = "", userwin, passwin
-          // if user2 is set, winning user name and password is 2
-          if(user2)
-            userwin = user2, passwin = pass2
-          // if user1 is set, and user2 is not set, user1 and pass1 win
-          if(user1 && ! user2)
-            userwin = user1, passwin = pass1
-          // if neither user name is set, pass 2 wins (there will be no userinfo in URL)
-          if (!(user1 || user2))
-            passwin = pass2
-          if(user1) { // should add userinfo to URL (there is no way to create userinfo part of URL with only password in)
-            urlstart = "http://" + user1
-            if(pass1)
-              urlstart += ":" + pass1
-            urlstart += "@" + location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+          const client = new XMLHttpRequest(),
+                userwin = user2 || user1,
+                passwin = pass2 || pass1;
+          let urlstart = "";
+          if (user1 || pass1) {
+            urlstart = "http://";
+            if (user1) {
+              urlstart += user1;
+            }
+            if (pass1) {
+              urlstart += ":" + pass1;
+            }
+            urlstart += "@" + location.host + location.pathname.replace(/\/[^\/]*$/, '/');
           }
-          client.open("GET", urlstart + "resources/authentication.py", false, user2, pass2)
-          client.setRequestHeader("x-user", userwin)
-          client.send(null)
-          assert_true(client.responseText == ((userwin||'') + "\n" + (passwin||'')), 'responseText should contain the right user and password')
-
-          // We want to send multiple requests to the same realm here, so we try to make the UA forget its (cached) credentials between each test..
-          // forcing a 401 response to (hopefully) "log out"
-          // NOTE: This is commented out because it causes authentication prompts while running the test
-          //client.open('GET', "resources/authentication.py?logout=1", false)
-          //client.send()
-        }, document.title+' '+name)
+          client.open("GET", urlstart + "resources/authentication.py", false, user2, pass2);
+          client.setRequestHeader("x-user", userwin);
+          client.send(null);
+          assert_equals(client.responseText, ((userwin||'') + "\n" + (passwin||'')), 'responseText should contain the right user and password');
+        }, "XMLHttpRequest user/pass options: " + name);
       }
-      request(null, null, token(), token(), 'user/pass in open() call')
-      request(null, null, token(), token(), 'another user/pass in open() call - must override cached credentials from previous test')
-      request("userinfo-user", "userinfo-pass", token(), token(), 'user/pass both in URL userinfo AND open() call - expexted that open() wins')
-      request(token(), token(), null, null, 'user/pass *only* in URL userinfo')
-      request(token(), null, null, token(), 'user name in URL userinfo, password in open() call: user name wins and password is thrown away')
-      request("1", token(), token(), null, 'user name and password in URL userinfo, only user name in open() call: user name in open() wins')
+      // Cannot have just a password
+      request(null, null, token(), null, "user in open()");
+      request(null, null, token(), token(), "user/pass in open()");
+      request(null, null, token(), token(), "another user/pass in open(); must override cached credentials from previous test");
+      request(null, token(), token(), null, "pass in URL, user in open()");
+      request(null, token(), token(), token(), "pass in URL, user/pass in open()");
+      request(token(), null, null, null, "user in URL");
+      request(token(), null, null, token(), "user in URL, pass in open()");
+      request(token(), token(), null, null, "user/pass in URL");
+      request(token(), null, token(), null, "user in URL and open()");
+      request(token(), null, token(), token(), "user in URL; user/pass in open()");
+      request(token(), token(), token(), null, "user/pass in URL; user in open()");
+      request(token(), token(), null, token(), "user/pass in URL; pass in open()");
+      request(token(), token(), token(), token(), "user/pass in URL and open()");
     </script>
   </body>
 </html>
diff --git a/xhr/send-content-type-charset.htm b/xhr/send-content-type-charset.htm
index 9e93279..4e75df2 100644
--- a/xhr/send-content-type-charset.htm
+++ b/xhr/send-content-type-charset.htm
@@ -15,7 +15,7 @@
         test(function() {
         var client = new XMLHttpRequest()
         client.open("POST", "resources/content.py", false)
-        if(input)
+        if(input !== null)
           client.setRequestHeader("Content-Type", input)
         client.send("TEST")
         assert_equals(client.responseText, "TEST")
@@ -29,6 +29,11 @@
         "header with invalid MIME type is not changed"
       )
       request(
+        "",
+        "",
+        "header with invalid MIME type (empty string) is not changed"
+      )
+      request(
         "charset=ascii",
         "charset=ascii",
         "known charset but bogus header - missing MIME type"
@@ -40,7 +45,7 @@
       )
       request(
         "text/plain;charset=utf-8",
-        "text/plain;charset=utf-8",
+        "text/plain;charset=UTF-8",
         "Correct text/plain MIME with charset"
       )
       request(
@@ -54,13 +59,18 @@
         "If no charset= param is given, implementation should not add one - known MIME"
       )
       request(
+        "text/plain;  hi=bye",
+        "text/plain;  hi=bye",
+        "If no charset= param is given, implementation should not add one - known MIME, unknown param, two spaces"
+      )
+      request(
         "text/x-thepiano;charset= waddup",
         "text/x-thepiano;charset=UTF-8",
         "charset given but wrong, fix it (unknown MIME, bogus charset)"
       )
       request(
         "text/plain;charset=utf-8;charset=waddup",
-        "text/plain;charset=utf-8;charset=UTF-8",
+        "text/plain;charset=UTF-8",
         "charset given but wrong, fix it (known MIME, bogus charset)"
       )
       request(
@@ -70,14 +80,34 @@
       )
       request(
         "text/x-pink-unicorn; charset=windows-1252; charset=bogus; notrelated; charset=ascii",
-        "text/x-pink-unicorn; charset=UTF-8; charset=UTF-8; notrelated; charset=UTF-8",
-        "If multiple charset parameters are given, all should be rewritten"
+        "text/x-pink-unicorn;charset=UTF-8",
+        "Multiple charset parameters deduplicate, bogus parameter dropped"
       )
       request(
         null,
         "text/plain;charset=UTF-8",
         "No content type set, give MIME and charset"
       )
+      request(
+        "text/plain;charset= utf-8",
+        "text/plain;charset=UTF-8",
+        "charset with space")
+      request(
+        "text/plain;charset=\"utf-8\"",
+        "text/plain;charset=UTF-8",
+        "charset in double quotes")
+      request(
+        "text/plain;charset=\" utf-8\"",
+        "text/plain;charset=UTF-8",
+        "charset in double quotes with space")
+      request(
+        "text/plain;charset=\"u\\t\\f-8\"",
+        "text/plain;charset=UTF-8",
+        "charset in double quotes with backslashes")
+      request(
+        "YO/yo;charset=x;yo=YO; X=y",
+        "yo/yo;charset=UTF-8;yo=YO;x=y",
+        "unknown parameters need to be preserved")
     </script>
   </body>
 </html>
diff --git a/xhr/send-send.any.js b/xhr/send-send.any.js
new file mode 100644
index 0000000..2bfe404
--- /dev/null
+++ b/xhr/send-send.any.js
@@ -0,0 +1,7 @@
+test(function() {
+  var client = new XMLHttpRequest()
+  client.open("GET", "resources/well-formed.xml")
+  client.send(null)
+  assert_throws("InvalidStateError", function() { client.send(null) })
+  client.abort()
+}, "XMLHttpRequest: send() - send()");
diff --git a/xhr/send-send.htm b/xhr/send-send.htm
deleted file mode 100644
index cbcbdb4..0000000
--- a/xhr/send-send.htm
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <title>XMLHttpRequest: send() - send()</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol/li[2]" />
-  </head>
-  <body>
-    <div id="log"></div>
-    <script src="send-send.js"></script>
-  </body>
-</html>
diff --git a/xhr/send-send.js b/xhr/send-send.js
deleted file mode 100644
index 2e7fe86..0000000
--- a/xhr/send-send.js
+++ /dev/null
@@ -1,7 +0,0 @@
-test(function() {
-  var client = new XMLHttpRequest()
-  client.open("GET", "resources/well-formed.xml")
-  client.send(null)
-  assert_throws("InvalidStateError", function() { client.send(null) })
-  client.abort()
-})
diff --git a/xhr/send-send.worker.js b/xhr/send-send.worker.js
deleted file mode 100644
index 9d34ce6..0000000
--- a/xhr/send-send.worker.js
+++ /dev/null
@@ -1,3 +0,0 @@
-importScripts("/resources/testharness.js");
-importScripts("send-send.js");
-done();
diff --git a/xhr/send-timeout-events.htm b/xhr/send-timeout-events.htm
index 6aea627..eae2568 100644
--- a/xhr/send-timeout-events.htm
+++ b/xhr/send-timeout-events.htm
@@ -4,66 +4,52 @@
     <script src="/resources/testharness.js"></script>
     <script src="/resources/testharnessreport.js"></script>
     <title>XMLHttpRequest: The send() method: timeout is not 0 </title>
-    <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
-    <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[5] following::a[contains(@href,'#timeout-error')]/.." />
-    <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".." />
-    <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[7]/ol/li[3] following::ol[1]/li[7]/ol/li[4] following::ol[1]/li[9] following::ol[1]/li[10]" />
 </head>
 
 <body>
     <div id="log"></div>
 
     <script type="text/javascript">
-        var test = async_test();
+        async_test(t => {
+            const xhr = new XMLHttpRequest(),
+                  expect = [4, "", "upload.timeout", "upload.loadend", "timeout", "loadend"];
+            let actual = [];
 
-        test.step(function()
-        {
-            var xhr = new XMLHttpRequest();
-            var expect = [4, "", "upload.timeout", "upload.loadend", "timeout", "loadend"];
-            var actual = [];
-
-            xhr.onreadystatechange = test.step_func(function()
-            {
-                if (xhr.readyState == 4)
-                {
+            xhr.onreadystatechange = t.step_func(() => {
+                if (xhr.readyState == 4) {
                     actual.push(xhr.readyState, xhr.response);
                 }
             });
 
-            xhr.onloadend = test.step_func_done(function(e)
-            {
+            xhr.onloadend = t.step_func_done(e => {
                 assert_equals(e.loaded, 0);
                 assert_equals(e.total, 0);
                 actual.push(e.type);
                 assert_array_equals(actual, expect);
             });
 
-            xhr.ontimeout = test.step_func(function(e)
-            {
+            xhr.ontimeout = t.step_func(e => {
                 assert_equals(e.loaded, 0);
                 assert_equals(e.total, 0);
                 actual.push(e.type);
             });
 
 
-            xhr.upload.onloadend = test.step_func(function(e)
-            {
+            xhr.upload.onloadend = t.step_func(e => {
                 assert_equals(e.loaded, 0);
                 assert_equals(e.total, 0);
                 actual.push("upload." + e.type);
             });
 
-            xhr.upload.ontimeout = test.step_func(function(e)
-            {
+            xhr.upload.ontimeout = t.step_func(e => {
                 assert_equals(e.loaded, 0);
                 assert_equals(e.total, 0);
                 actual.push("upload." + e.type);
             });
 
 
-            var content = "";
-            for (var i = 0; i < 121026; i++)
-            {
+            let content = "";
+            for (var i = 0; i < 121026; i++) {
                 content += "[" + i + "]";
             }
 
diff --git a/xhr/xmlhttprequest-closing-worker.html b/xhr/xmlhttprequest-closing-worker.html
deleted file mode 100644
index 5c8142f..0000000
--- a/xhr/xmlhttprequest-closing-worker.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!--
-self.close()
-var xhr = new XMLHttpRequest();
-xhr.open("GET", "/resources/testharness.js");
-xhr.send(42);
-postMessage(xhr.readyState)
-/*-->
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8" />
-    <title>XHR used when worker is closing itself</title>
-    <script src="/resources/testharness.js"></script>
-    <script src="/resources/testharnessreport.js"></script>
-  </head>
-  <body>
-    <div id="log"></div>
-    <script>
-      var test = async_test();
-      test.step(function() {
-        var worker = new Worker('#')
-        worker.onmessage = function(e) {
-          test.step(function() {
-            assert_equals(e.data, XMLHttpRequest.OPENED, 'XHR.readyState')
-          })
-          test.done()
-        }
-      })
-    </script>
-  </body>
-</html>
-<!--*/ //-->