Skip to content

Commit d9d0442

Browse files
author
Takashi Kato
committed
Merge branch 'develop'
2 parents 0e2fea1 + 0ed1f21 commit d9d0442

File tree

9 files changed

+324
-69
lines changed

9 files changed

+324
-69
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
This plugin makes it easy to test ES modules with [importmap-rails](https://github.com/rails/importmap-rails) when using Rails 7 or later.
44
It integrates the [Mocha](https://mochajs.org/) JavaScript testing library (using [Chai](https://www.chaijs.com/) as the assertion library, [@mswjs/interceptors](https://github.com/mswjs/interceptors) as the mocking library) and runs tests for ES modules delivered with importmap in the browser.
55

6+
| Library | Version |
7+
|-------------------------------------------|---------|
8+
| [Mocha](https://mochajs.org/) | 11.1.0 |
9+
| [Chai](https://www.chaijs.com/) | 5.1.2 |
10+
| [@mswjs/interceptors](https://github.com/mswjs/interceptors) | 0.37.5 |
11+
12+
[More useful in combination with the rails_live_reload gem](#use-with-rails_live_reload-gem)
13+
614
# Installation
715

816
Assuming you have already installed importmap-rails with Rails 7, add the following to your Gemfile and run `bundle install`.
@@ -85,6 +93,39 @@ describe('clear controller', () => {
8593
* config.importmap_mocha_path: The location where the test code is stored. Default is `test/javascripts` and `spec/javascripts`.
8694
* config.importmap_mocha_scripts: The scripts to be loaded globally. e.g. `['jquery.js']`.
8795

96+
# Use with Rails_Live_Reload gem
97+
98+
It is strongly recommended to use with [rails_live_reload](https://github.com/railsjazz/rails_live_reload)
99+
100+
![](./images/screencast01.gif)
101+
102+
Add this line to your application's Gemfile:
103+
104+
```ruby
105+
group :development do
106+
gem "importmap_mocha_rails"
107+
gem "rails_live_reload"
108+
end
109+
```
110+
111+
And then execute:
112+
113+
```
114+
bundle install
115+
rails generate rails_live_reload:install
116+
```
117+
118+
Edit initializer
119+
```ruby
120+
# frozen_string_literal: true
121+
122+
RailsLiveReload.configure do |config|
123+
config.watch %r{app/views/.+\.(erb|haml|slim)$}
124+
# Monitor JavaScript tests in addition to default paths
125+
config.watch %r{(app|vendor|test)/(assets|javascript|javascripts)/\w+/(.+\.(css|js|html|png|jpg|ts|jsx)).*}, reload: :always
126+
end if defined?(RailsLiveReload)
127+
```
128+
88129
# Author
89130

90131
Takashi Kato tohosaku@users.osdn.me
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export function changeFavicon(failures) {
2+
const links = document.getElementsByTagName('link')
3+
4+
for (let i=0; i<links.length; i++) {
5+
let link = links[i];
6+
if (link.rel == 'icon') {
7+
const icon = failures > 0 ? favicon('red')
8+
: favicon('green')
9+
link.remove()
10+
const newlink = document.createElement("link");
11+
newlink.rel = 'icon';
12+
newlink.href = icon;
13+
newlink.type = 'image/svg+xml';
14+
const head = document.getElementsByTagName("head")[0];
15+
head.appendChild(newlink);
16+
17+
return;
18+
}
19+
}
20+
}
21+
22+
function favicon(color, count) {
23+
const icon = `<?xml version="1.0" encoding="UTF-8"?>
24+
<svg width="100" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg">
25+
<style>circle {
26+
fill: ${color};
27+
stroke: ${color};
28+
stroke-width: 3px;
29+
}
30+
</style>
31+
<circle cx="50" cy="50" r="47"/>
32+
</svg>`
33+
const base64Svg = btoa(unescape(encodeURIComponent(icon)));
34+
35+
return `data:image/svg+xml;base64,${base64Svg}`;
36+
}

app/views/importmap_mocha/test/index.html.erb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<% if Rails.application.config.importmap_mocha_scripts.size > 0 %>
77
<%= javascript_include_tag *Rails.application.config.importmap_mocha_scripts %>
88
<% end %>
9-
<%= favicon_link_tag %>
9+
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
1010
<%= javascript_include_tag 'mocha' %>
1111
<%= stylesheet_link_tag 'mocha' %>
1212
<%= javascript_importmap_tags 'importmap_mocha' %>
@@ -25,8 +25,10 @@
2525
</div>
2626
</div>
2727
</div>
28-
<script type="module">
29-
mocha.run();
30-
</script>
28+
<script type="module">
29+
import { changeFavicon } from 'importmap_mocha'
30+
31+
mocha.run(changeFavicon)
32+
</script>
3133
</body>
3234
</html>

images/screencast01.gif

1.61 MB
Loading

importmap_mocha-rails.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ Gem::Specification.new do |spec|
2020
Dir['app/**/*', 'config/**/*', 'lib/**/*', 'vendor/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md']
2121
end
2222

23-
spec.add_dependency 'importmap-rails', '~> 2.0.0'
23+
spec.add_dependency 'importmap-rails', '>= 2.0.0'
2424
end

lib/importmap_mocha/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module ImportmapMocha
4-
VERSION = '0.3.5'
4+
VERSION = '0.3.6'
55
end

vendor/javascripts/@mswjs--interceptors--presets--browser.js

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -837,23 +837,6 @@ function isPropertyAccessible(obj, key) {
837837
}
838838

839839
// src/utils/responseUtils.ts
840-
var RESPONSE_STATUS_CODES_WITHOUT_BODY = /* @__PURE__ */ new Set([
841-
101,
842-
103,
843-
204,
844-
205,
845-
304
846-
]);
847-
var RESPONSE_STATUS_CODES_WITH_REDIRECT = /* @__PURE__ */ new Set([
848-
301,
849-
302,
850-
303,
851-
307,
852-
308
853-
]);
854-
function isResponseWithoutBody(status) {
855-
return RESPONSE_STATUS_CODES_WITHOUT_BODY.has(status);
856-
}
857840
function createServerErrorResponse(body) {
858841
return new Response(
859842
JSON.stringify(
@@ -922,13 +905,17 @@ async function handleRequest(options) {
922905
});
923906
const requestAbortPromise = new DeferredPromise();
924907
if (options.request.signal) {
925-
options.request.signal.addEventListener(
926-
"abort",
927-
() => {
928-
requestAbortPromise.reject(options.request.signal.reason);
929-
},
930-
{ once: true }
931-
);
908+
if (options.request.signal.aborted) {
909+
requestAbortPromise.reject(options.request.signal.reason);
910+
} else {
911+
options.request.signal.addEventListener(
912+
"abort",
913+
() => {
914+
requestAbortPromise.reject(options.request.signal.reason);
915+
},
916+
{ once: true }
917+
);
918+
}
932919
}
933920
const result = await until(async () => {
934921
const requestListtenersPromise = emitAsync(options.emitter, "request", {
@@ -1147,6 +1134,12 @@ function hasConfigurableGlobal(propertyName) {
11471134
if (typeof descriptor === "undefined") {
11481135
return false;
11491136
}
1137+
if (typeof descriptor.get === "function" && typeof descriptor.get() === "undefined") {
1138+
return false;
1139+
}
1140+
if (typeof descriptor.get === "undefined" && descriptor.value == null) {
1141+
return false;
1142+
}
11501143
if (typeof descriptor.set === "undefined" && !descriptor.configurable) {
11511144
console.error(
11521145
`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`
@@ -1156,6 +1149,83 @@ function hasConfigurableGlobal(propertyName) {
11561149
return true;
11571150
}
11581151

1152+
// src/utils/fetchUtils.ts
1153+
var FetchResponse = class _FetchResponse extends Response {
1154+
static {
1155+
/**
1156+
* Response status codes for responses that cannot have body.
1157+
* @see https://fetch.spec.whatwg.org/#statuses
1158+
*/
1159+
this.STATUS_CODES_WITHOUT_BODY = [101, 103, 204, 205, 304];
1160+
}
1161+
static {
1162+
this.STATUS_CODES_WITH_REDIRECT = [301, 302, 303, 307, 308];
1163+
}
1164+
static isConfigurableStatusCode(status) {
1165+
return status >= 200 && status <= 599;
1166+
}
1167+
static isRedirectResponse(status) {
1168+
return _FetchResponse.STATUS_CODES_WITH_REDIRECT.includes(status);
1169+
}
1170+
/**
1171+
* Returns a boolean indicating whether the given response status
1172+
* code represents a response that can have a body.
1173+
*/
1174+
static isResponseWithBody(status) {
1175+
return !_FetchResponse.STATUS_CODES_WITHOUT_BODY.includes(status);
1176+
}
1177+
static setUrl(url, response) {
1178+
if (!url) {
1179+
return;
1180+
}
1181+
if (response.url != "") {
1182+
return;
1183+
}
1184+
Object.defineProperty(response, "url", {
1185+
value: url,
1186+
enumerable: true,
1187+
configurable: true,
1188+
writable: false
1189+
});
1190+
}
1191+
/**
1192+
* Parses the given raw HTTP headers into a Fetch API `Headers` instance.
1193+
*/
1194+
static parseRawHeaders(rawHeaders) {
1195+
const headers = new Headers();
1196+
for (let line = 0; line < rawHeaders.length; line += 2) {
1197+
headers.append(rawHeaders[line], rawHeaders[line + 1]);
1198+
}
1199+
return headers;
1200+
}
1201+
constructor(body, init = {}) {
1202+
const status = init.status ?? 200;
1203+
const safeStatus = _FetchResponse.isConfigurableStatusCode(status) ? status : 200;
1204+
const finalBody = _FetchResponse.isResponseWithBody(status) ? body : null;
1205+
super(finalBody, {
1206+
...init,
1207+
status: safeStatus
1208+
});
1209+
if (status !== safeStatus) {
1210+
const stateSymbol = Object.getOwnPropertySymbols(this).find(
1211+
(symbol) => symbol.description === "state"
1212+
);
1213+
if (stateSymbol) {
1214+
const state = Reflect.get(this, stateSymbol);
1215+
Reflect.set(state, "status", status);
1216+
} else {
1217+
Object.defineProperty(this, "status", {
1218+
value: status,
1219+
enumerable: true,
1220+
configurable: true,
1221+
writable: false
1222+
});
1223+
}
1224+
}
1225+
_FetchResponse.setUrl(init.url, this);
1226+
}
1227+
};
1228+
11591229
// src/interceptors/fetch/index.ts
11601230
var FetchInterceptor = class _FetchInterceptor extends Interceptor {
11611231
static {
@@ -1195,8 +1265,9 @@ var FetchInterceptor = class _FetchInterceptor extends Interceptor {
11951265
rawResponse
11961266
});
11971267
const decompressedStream = decompressResponse(rawResponse);
1198-
const response = decompressedStream === null ? rawResponse : new Response(decompressedStream, rawResponse);
1199-
if (RESPONSE_STATUS_CODES_WITH_REDIRECT.has(response.status)) {
1268+
const response = decompressedStream === null ? rawResponse : new FetchResponse(decompressedStream, rawResponse);
1269+
FetchResponse.setUrl(request.url, response);
1270+
if (FetchResponse.isRedirectResponse(response.status)) {
12001271
if (request.redirect === "error") {
12011272
responsePromise.reject(createNetworkError("unexpected redirect"));
12021273
return;
@@ -1213,12 +1284,6 @@ var FetchInterceptor = class _FetchInterceptor extends Interceptor {
12131284
return;
12141285
}
12151286
}
1216-
Object.defineProperty(response, "url", {
1217-
writable: false,
1218-
enumerable: true,
1219-
configurable: false,
1220-
value: request.url
1221-
});
12221287
if (this.emitter.listenerCount("response") > 0) {
12231288
this.logger.info('emitting the "response" event...');
12241289
await emitAsync(this.emitter, "response", {
@@ -1477,8 +1542,9 @@ function parseJson(data) {
14771542

14781543
// src/interceptors/XMLHttpRequest/utils/createResponse.ts
14791544
function createResponse(request, body) {
1480-
const responseBodyOrNull = isResponseWithoutBody(request.status) ? null : body;
1481-
return new Response(responseBodyOrNull, {
1545+
const responseBodyOrNull = FetchResponse.isResponseWithBody(request.status) ? body : null;
1546+
return new FetchResponse(responseBodyOrNull, {
1547+
url: request.responseURL,
14821548
status: request.status,
14831549
statusText: request.statusText,
14841550
headers: createHeadersFromXMLHttpReqestHeaders(

0 commit comments

Comments
 (0)