diff --git a/src/browser/App.js b/src/browser/App.js
index ef4c094..186f51b 100644
--- a/src/browser/App.js
+++ b/src/browser/App.js
@@ -11,7 +11,7 @@ import SubmitButton from "./component/SubmitButton";
import RelatedListBox from "./component/RelatedListBox";
import ServiceList from "./component/ServiceList";
import AppContext from "./AppContext";
-import serviceManger from "./service-instance";
+import serviceManger, { waitForInitialization } from "./service-instance";
const ipcRenderer = require("electron").ipcRenderer;
const appContext = new AppContext();
@@ -26,9 +26,24 @@ class App extends React.Component {
},
appContext.ServiceStore.state
);
+
+ // サービスの初期化状態は別管理(ServiceStoreの変更で上書きされないように)
+ this._serviceInitialized = false;
}
- componentDidMount() {
+ async componentDidMount() {
+ // サービスの初期化を待つ
+ try {
+ await waitForInitialization();
+ this._serviceInitialized = true;
+ this.forceUpdate();
+ } catch (error) {
+ console.error("Failed to initialize services:", error);
+ // エラーが発生しても表示(シークレットがフォールバック値になる)
+ this._serviceInitialized = true;
+ this.forceUpdate();
+ }
+
// ServiceStore の変更を監視(componentDidMountで登録)
this.unsubscribe = appContext.ServiceStore.onChange(() => {
let newState = Object.assign({}, this.state, appContext.ServiceStore.state);
@@ -117,6 +132,19 @@ class App extends React.Component {
}
render() {
+ if (!this._serviceInitialized) {
+ return (
+
+ );
+ }
+
const { ServiceAction } = appContext;
const selectTags = ServiceAction.selectTags.bind(ServiceAction);
const updateTitle = ServiceAction.updateTitle.bind(ServiceAction);
@@ -197,6 +225,8 @@ class App extends React.Component {
appContext.on("dispatch", ({ eventKey }) => {
ipcRenderer.send(String(eventKey));
});
+
+// アプリを即座に起動(初期化はコンポーネント内で処理)
try {
const root = createRoot(document.getElementById("js-main"));
root.render();
diff --git a/src/browser/service-instance.js b/src/browser/service-instance.js
index d77a11c..31917d8 100644
--- a/src/browser/service-instance.js
+++ b/src/browser/service-instance.js
@@ -5,33 +5,75 @@ import ServiceManger from "./service-manager";
// FIXME: use IPC
const notBundledRequire = require;
+
+// 初期化が完了したかどうかのフラグとPromise
+let initialized = false;
+let initializationPromise = null;
const manager = new ServiceManger();
-const getServiceNameList = () => {
- if (process.env.PLAYWRIGHT_TEST === "1" || process.title?.includes("playwright")) {
- return notBundledRequire("../../tests/fixtures/test-services.js");
- } else {
- const serviceModule = notBundledRequire("../service.js");
- return serviceModule.default || serviceModule;
- }
-};
-const services = getServiceNameList()
- .filter((service) => {
- return service.enabled;
- })
- .map((service) => {
- const { Model, Client } = service.index ? service.index : require(service.indexPath);
- const client = new Client(service.options);
- return {
- model: new Model(),
- client: client,
- isDefaultChecked: service.isDefaultChecked && client.isLogin()
- };
- });
-services.forEach(({ model, client, isDefaultChecked }) => {
- manager.addService({
- model,
- client,
- isDefaultChecked
- });
-});
+
+// 非同期初期化関数
+async function initializeManager() {
+ if (initialized) return manager;
+ if (initializationPromise) return initializationPromise;
+
+ initializationPromise = (async () => {
+ try {
+ let serviceList;
+ if (process.env.PLAYWRIGHT_TEST === "1" || process.title?.includes("playwright")) {
+ serviceList = notBundledRequire("../../tests/fixtures/test-services.js");
+ } else {
+ const serviceModule = notBundledRequire("../service.js");
+ const serviceConfig = serviceModule.default || serviceModule;
+
+ // service.jsがPromiseを返す場合は解決する
+ if (serviceConfig && typeof serviceConfig.then === "function") {
+ serviceList = await serviceConfig;
+ } else {
+ serviceList = serviceConfig;
+ }
+ }
+
+ if (!Array.isArray(serviceList)) {
+ throw new Error("Service list is not an array");
+ }
+
+ const services = serviceList
+ .filter((service) => {
+ return service.enabled;
+ })
+ .map((service) => {
+ const { Model, Client } = service.index ? service.index : require(service.indexPath);
+ const client = new Client(service.options);
+ return {
+ model: new Model(),
+ client: client,
+ isDefaultChecked: service.isDefaultChecked && client.isLogin()
+ };
+ });
+
+ services.forEach(({ model, client, isDefaultChecked }) => {
+ manager.addService({
+ model,
+ client,
+ isDefaultChecked
+ });
+ });
+
+ initialized = true;
+ return manager;
+ } catch (error) {
+ console.error("Failed to initialize services:", error);
+ throw error;
+ }
+ })();
+
+ return initializationPromise;
+}
+
+// 初期化を待つためのヘルパー関数をエクスポート
+export async function waitForInitialization() {
+ return initializeManager();
+}
+
+// デフォルトエクスポートはmanagerのままだが、使用前に初期化が必要
export default manager;
diff --git a/tests/integration/app.play.js b/tests/integration/app.play.js
index 390516b..1712145 100644
--- a/tests/integration/app.play.js
+++ b/tests/integration/app.play.js
@@ -43,6 +43,13 @@ function createElectronLaunchOptions(additionalArgs = []) {
return { args, env, timeout: 30000 };
}
+// アプリケーションの初期化完了を待つヘルパー関数
+async function waitForAppInitialization(window) {
+ // ServiceListが表示されることで初期化完了を確認
+ const serviceList = window.locator(".ServiceList");
+ await expect(serviceList).toBeVisible({ timeout: 10000 });
+}
+
// Test contextを使用してElectronアプリケーションとウィンドウを管理
const test = baseTest.extend({
app: async ({}, use) => {
@@ -75,10 +82,44 @@ test.describe("Postem Application", () => {
// メインコンテナが存在することを確認
const mainDiv = window.locator("#js-main");
await expect(mainDiv).toBeVisible();
+
+ // ローディング画面が表示される可能性があるので、アプリが完全に初期化されるまで待つ
+ // ServiceListが表示されることで初期化完了を確認
+ const serviceList = window.locator(".ServiceList");
+ await expect(serviceList).toBeVisible({ timeout: 10000 });
+ });
+
+ test("ローディング画面が表示され、初期化後に消える", async ({ window }) => {
+ // ウィンドウが読み込まれるまで待機
+ await window.waitForLoadState("domcontentloaded");
+
+ // ローディング画面の存在を確認(すぐに確認)
+ const loadingText = window.locator("text=Loading...");
+ const isLoadingVisible = await loadingText.isVisible().catch(() => false);
+
+ // ローディング画面が表示されていた場合
+ if (isLoadingVisible) {
+ console.log("Loading screen was displayed");
+
+ // ServiceListが表示されるまで待つ(初期化完了)
+ const serviceList = window.locator(".ServiceList");
+ await expect(serviceList).toBeVisible({ timeout: 10000 });
+
+ // ローディング画面が消えたことを確認
+ await expect(loadingText).not.toBeVisible();
+ } else {
+ // ローディング画面が表示されなかった場合(高速な初期化)
+ console.log("Loading screen was not displayed (fast initialization)");
+
+ // ServiceListがすでに表示されていることを確認
+ const serviceList = window.locator(".ServiceList");
+ await expect(serviceList).toBeVisible();
+ }
});
test("エラーハンドリングテスト", async ({ window }) => {
await window.waitForLoadState("domcontentloaded");
+ await waitForAppInitialization(window);
// DEBUGサービスを選択
await window.locator(".ServiceList img").first().click();
@@ -105,6 +146,7 @@ test.describe("Postem Application", () => {
test("ウィンドウリサイズ対応テスト", async ({ window }) => {
await window.waitForLoadState("domcontentloaded");
+ await waitForAppInitialization(window);
// 初期サイズでの要素確認
await expect(window.locator(".ServiceList")).toBeVisible();
@@ -150,6 +192,7 @@ test.describe("Postem Application", () => {
testWithUrlParams("URLパラメーターからの起動テスト", async ({ windowWithUrlParams }) => {
await windowWithUrlParams.waitForLoadState("domcontentloaded");
+ await waitForAppInitialization(windowWithUrlParams);
// パラメーターが正しく反映されているかチェック
// URLフィールドの値を確認
@@ -159,6 +202,7 @@ test.describe("Postem Application", () => {
test("統合ワークフロー: Title + URL + Tag選択 + CodeMirror + Cmd+Enter", async ({ window }) => {
await window.waitForLoadState("domcontentloaded");
+ await waitForAppInitialization(window);
// 1. DEBUGサービスを選択
await window.locator(".ServiceList img").first().click();
@@ -225,6 +269,7 @@ test.describe("Postem Application", () => {
test("textlint機能の動作確認テスト", async ({ window }) => {
await window.waitForLoadState("domcontentloaded");
+ await waitForAppInitialization(window);
// DEBUGサービスを選択
await window.locator(".ServiceList img").first().click();