diff --git a/Tests/LibWeb/Text/expected/wpt-import/resize-observer/observe.txt b/Tests/LibWeb/Text/expected/wpt-import/resize-observer/observe.txt
new file mode 100644
index 00000000000..ea20142331f
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/wpt-import/resize-observer/observe.txt
@@ -0,0 +1,27 @@
+Harness status: OK
+
+Found 21 tests
+
+10 Pass
+11 Fail
+Pass ResizeObserver implemented
+Pass guard
+Pass test0: simple observation
+Pass test1: multiple observation on same element trigger only one
+Pass test2: throw exception when observing non-element
+Pass test3: disconnect stops all notifications
+Pass test4: unobserve target stops notifications, unobserve non-observed does nothing
+Fail test5: observe img
+Pass test6: iframe notifications
+Pass test7: callback.this
+Fail test8: simple content-box observation
+Fail test9: simple content-box observation but keep border-box size unchanged
+Fail test10: simple border-box observation
+Fail test11: simple observation with vertical writing mode
+Fail test12: no observation is fired after the change of writing mode when box's specified size comes from logical size properties.
+Fail test13: an observation is fired after the change of writing mode when box's specified size comes from physical size properties.
+Fail test14: observe the same target but using a different box should override the previous one
+Fail test15: an observation is fired with box dimensions 0 when element's display property is set to inline
+Fail test16: observations fire once with 0x0 size for non-replaced inline elements
+Fail test17: Box sizing snd Resize Observer notifications
+Pass test18: an observation is fired when device-pixel-content-box is being observed
\ No newline at end of file
diff --git a/Tests/LibWeb/Text/input/wpt-import/resize-observer/observe.html b/Tests/LibWeb/Text/input/wpt-import/resize-observer/observe.html
new file mode 100644
index 00000000000..e554f48bee3
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/resize-observer/observe.html
@@ -0,0 +1,1007 @@
+
+
ResizeObserver tests
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/iframe.html b/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/iframe.html
new file mode 100644
index 00000000000..5c801e63682
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/iframe.html
@@ -0,0 +1,38 @@
+
+
+
+
+iframe test
+t1
+
diff --git a/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/resizeTestHelper.js b/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/resizeTestHelper.js
new file mode 100644
index 00000000000..284a780c259
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/resize-observer/resources/resizeTestHelper.js
@@ -0,0 +1,195 @@
+'use strict';
+
+/**
+ ResizeTestHelper is a framework to test ResizeObserver
+ notifications. Use it to make assertions about ResizeObserverEntries.
+ This framework is needed because ResizeObservations are
+ delivered asynchronously inside the event loop.
+
+ Features:
+ - can queue multiple notification steps in a test
+ - handles timeouts
+ - returns Promise that is fulfilled when test completes.
+ Use to chain tests (since parallel async ResizeObserver tests
+ would conflict if reusing same DOM elements).
+
+ Usage:
+
+ create ResizeTestHelper for every test.
+ Make assertions inside notify, timeout callbacks.
+ Start tests with helper.start()
+ Chain tests with Promises.
+ Counts animation frames, see startCountingRaf
+*/
+
+/*
+ @param name: test name
+ @param steps:
+ {
+ setup: function(ResizeObserver) {
+ // called at the beginning of the test step
+ // your observe/resize code goes here
+ },
+ notify: function(entries, observer) {
+ // ResizeObserver callback.
+ // Make assertions here.
+ // Return true if next step should start on the next event loop.
+ },
+ timeout: function() {
+ // Define this if your test expects to time out, and the expected timeout
+ // value will be 100ms.
+ // If undefined, timeout is assert_unreached after 1000ms.
+ }
+ }
+*/
+function ResizeTestHelper(name, steps)
+{
+ this._name = name;
+ this._steps = steps || [];
+ this._stepIdx = -1;
+ this._harnessTest = null;
+ this._observer = new ResizeObserver(this._handleNotification.bind(this));
+ this._timeoutBind = this._handleTimeout.bind(this);
+ this._nextStepBind = this._nextStep.bind(this);
+}
+
+// The default timeout value in ms.
+// We expect TIMEOUT to be longer than we would ever have to wait for notify()
+// to be fired. This is used for tests which are not expected to time out, so
+// it can be large without slowing down the test.
+ResizeTestHelper.TIMEOUT = 1000;
+// A reasonable short timeout value in ms.
+// We expect SHORT_TIMEOUT to be long enough that notify() would usually get a
+// chance to fire before SHORT_TIMEOUT expires. This is used for tests which
+// *are* expected to time out (with no callbacks fired), so we'd like to keep
+// it relatively short to avoid slowing down the test.
+ResizeTestHelper.SHORT_TIMEOUT = 100;
+
+ResizeTestHelper.prototype = {
+ get _currentStep() {
+ return this._steps[this._stepIdx];
+ },
+
+ _nextStep: function() {
+ if (++this._stepIdx == this._steps.length)
+ return this._done();
+ // Use SHORT_TIMEOUT if this step expects timeout.
+ let timeoutValue = this._steps[this._stepIdx].timeout ?
+ ResizeTestHelper.SHORT_TIMEOUT :
+ ResizeTestHelper.TIMEOUT;
+ this._timeoutId = this._harnessTest.step_timeout(
+ this._timeoutBind, timeoutValue);
+ try {
+ this._steps[this._stepIdx].setup(this._observer);
+ }
+ catch(err) {
+ this._harnessTest.step(() => {
+ assert_unreached("Caught a throw, possible syntax error");
+ });
+ }
+ },
+
+ _handleNotification: function(entries) {
+ if (this._timeoutId) {
+ window.clearTimeout(this._timeoutId);
+ delete this._timeoutId;
+ }
+ this._harnessTest.step(() => {
+ try {
+ let rafDelay = this._currentStep.notify(entries, this._observer);
+ if (rafDelay)
+ window.requestAnimationFrame(this._nextStepBind);
+ else
+ this._nextStep();
+ }
+ catch(err) {
+ this._harnessTest.step(() => {
+ throw err;
+ });
+ // Force to _done() the current test.
+ this._done();
+ }
+ });
+ },
+
+ _handleTimeout: function() {
+ delete this._timeoutId;
+ this._harnessTest.step(() => {
+ if (this._currentStep.timeout) {
+ this._currentStep.timeout();
+ }
+ else {
+ this._harnessTest.step(() => {
+ assert_unreached("Timed out waiting for notification. (" + ResizeTestHelper.TIMEOUT + "ms)");
+ });
+ }
+ this._nextStep();
+ });
+ },
+
+ _done: function() {
+ this._observer.disconnect();
+ delete this._observer;
+ this._harnessTest.done();
+ if (this._rafCountRequest) {
+ window.cancelAnimationFrame(this._rafCountRequest);
+ delete this._rafCountRequest;
+ }
+ window.requestAnimationFrame(() => { this._resolvePromise(); });
+ },
+
+ start: function(cleanup) {
+ this._harnessTest = async_test(this._name);
+
+ if (cleanup) {
+ this._harnessTest.add_cleanup(cleanup);
+ }
+
+ this._harnessTest.step(() => {
+ assert_equals(this._stepIdx, -1, "start can only be called once");
+ this._nextStep();
+ });
+ return new Promise( (resolve, reject) => {
+ this._resolvePromise = resolve;
+ this._rejectPromise = reject;
+ });
+ },
+
+ get rafCount() {
+ if (!this._rafCountRequest)
+ throw "rAF count is not active";
+ return this._rafCount;
+ },
+
+ get test() {
+ if (!this._harnessTest) {
+ throw "_harnessTest is not initialized";
+ }
+ return this._harnessTest;
+ },
+
+ _incrementRaf: function() {
+ if (this._rafCountRequest) {
+ this._rafCount++;
+ this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
+ }
+ },
+
+ startCountingRaf: function() {
+ if (this._rafCountRequest)
+ window.cancelAnimationFrame(this._rafCountRequest);
+ if (!this._incrementRafBind)
+ this._incrementRafBind = this._incrementRaf.bind(this);
+ this._rafCount = 0;
+ this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
+ }
+}
+
+function createAndAppendElement(tagName, parent) {
+ if (!parent) {
+ parent = document.body;
+ }
+ const element = document.createElement(tagName);
+ parent.appendChild(element);
+ return element;
+}