mobile-snap 1.0.4 ā 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.html +62 -62
- package/README.md +83 -78
- package/bin/cli.js +48 -48
- package/package.json +1 -1
package/README.html
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>MobileSnap -
|
|
6
|
+
<title>MobileSnap - Documentation & Architecture</title>
|
|
7
7
|
<!-- Google Fonts -->
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -586,7 +586,7 @@
|
|
|
586
586
|
<div class="header-glow"></div>
|
|
587
587
|
<div class="container">
|
|
588
588
|
<h1>MobileSnap šø</h1>
|
|
589
|
-
<p class="tagline">
|
|
589
|
+
<p class="tagline">Automate pixel-precise App Store & Google Play screenshots directly from your local development server using NPX.</p>
|
|
590
590
|
|
|
591
591
|
<div class="badge-container">
|
|
592
592
|
<div class="badge">
|
|
@@ -603,10 +603,10 @@
|
|
|
603
603
|
</header>
|
|
604
604
|
|
|
605
605
|
<div class="container">
|
|
606
|
-
<!--
|
|
606
|
+
<!-- Sample Output Mockups -->
|
|
607
607
|
<section class="glass-card">
|
|
608
|
-
<h2>š±
|
|
609
|
-
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
|
|
608
|
+
<h2>š± Sample Output (Premium Mockups)</h2>
|
|
609
|
+
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">Real-world visualization of screenshots automatically wrapped in premium device mockup frames using the <code>-m</code> or <code>--mockup</code> option:</p>
|
|
610
610
|
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin-top: 1.5rem;">
|
|
611
611
|
<div style="text-align: center; max-width: 45%; min-width: 280px;">
|
|
612
612
|
<h4 style="margin-bottom: 0.5rem; color: #fff;">iOS (iPhone 6.7" Pro Max)</h4>
|
|
@@ -619,38 +619,38 @@
|
|
|
619
619
|
</div>
|
|
620
620
|
</section>
|
|
621
621
|
|
|
622
|
-
<!--
|
|
622
|
+
<!-- Workflow Architecture -->
|
|
623
623
|
<section class="glass-card">
|
|
624
|
-
<h2>šļø
|
|
625
|
-
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">MobileSnap
|
|
624
|
+
<h2>šļø Workflow Architecture</h2>
|
|
625
|
+
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">MobileSnap is built using <strong>Commander</strong> for a rich CLI interface, and the <strong>Playwright Node API</strong> to control Chromium sequentially across various dimensions.</p>
|
|
626
626
|
|
|
627
627
|
<div class="arch-flow">
|
|
628
628
|
<div class="arch-node">
|
|
629
629
|
<div class="node-num">1</div>
|
|
630
630
|
<h4>CLI Parser</h4>
|
|
631
|
-
<p>
|
|
631
|
+
<p>Parses URL parameters, routes (paths), target platforms, and the output directory.</p>
|
|
632
632
|
</div>
|
|
633
633
|
<div class="arch-node">
|
|
634
634
|
<div class="node-num">2</div>
|
|
635
635
|
<h4>Browser Engine</h4>
|
|
636
|
-
<p>
|
|
636
|
+
<p>Launches Chromium in an isolated and asynchronous instance.</p>
|
|
637
637
|
</div>
|
|
638
638
|
<div class="arch-node">
|
|
639
639
|
<div class="node-num">3</div>
|
|
640
640
|
<h4>Viewport Emulation</h4>
|
|
641
|
-
<p>
|
|
641
|
+
<p>Dynamically emulates Retina (iOS) & Android display dimensions.</p>
|
|
642
642
|
</div>
|
|
643
643
|
<div class="arch-node">
|
|
644
644
|
<div class="node-num">4</div>
|
|
645
645
|
<h4>Hydration Check</h4>
|
|
646
|
-
<p>
|
|
646
|
+
<p>Waits for network idle state to ensure JavaScript rendering is fully complete.</p>
|
|
647
647
|
</div>
|
|
648
648
|
</div>
|
|
649
649
|
</section>
|
|
650
650
|
|
|
651
|
-
<!-- Target
|
|
651
|
+
<!-- Target Dimensions Requirement -->
|
|
652
652
|
<section class="glass-card">
|
|
653
|
-
<h2>š±
|
|
653
|
+
<h2>š± Target Screen Dimensions (Store Requirements)</h2>
|
|
654
654
|
|
|
655
655
|
<h3>š iOS (Apple App Store)</h3>
|
|
656
656
|
<div class="device-showcase">
|
|
@@ -662,8 +662,8 @@
|
|
|
662
662
|
<div class="device-info">
|
|
663
663
|
<h4>iPhone 6.7" Display</h4>
|
|
664
664
|
<div class="resolution">1290 x 2796 pixels</div>
|
|
665
|
-
<p>
|
|
666
|
-
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">
|
|
665
|
+
<p>Used for the latest iPhone Max/Pro Max models.</p>
|
|
666
|
+
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">File: 6.7_inch_[path].png</p>
|
|
667
667
|
</div>
|
|
668
668
|
</div>
|
|
669
669
|
|
|
@@ -675,8 +675,8 @@
|
|
|
675
675
|
<div class="device-info">
|
|
676
676
|
<h4>iPhone 6.5" Display</h4>
|
|
677
677
|
<div class="resolution">1242 x 2688 pixels</div>
|
|
678
|
-
<p>
|
|
679
|
-
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">
|
|
678
|
+
<p>Used for the iPhone Xs Max / 11 Pro Max series.</p>
|
|
679
|
+
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">File: 6.5_inch_[path].png</p>
|
|
680
680
|
</div>
|
|
681
681
|
</div>
|
|
682
682
|
</div>
|
|
@@ -691,8 +691,8 @@
|
|
|
691
691
|
<div class="device-info">
|
|
692
692
|
<h4>Android Phone</h4>
|
|
693
693
|
<div class="resolution">1080 x 2400 pixels</div>
|
|
694
|
-
<p>
|
|
695
|
-
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">
|
|
694
|
+
<p>Modern 20:9 aspect ratio for Google Play Phone screenshots.</p>
|
|
695
|
+
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">File: android_phone_[path].png</p>
|
|
696
696
|
</div>
|
|
697
697
|
</div>
|
|
698
698
|
|
|
@@ -704,17 +704,17 @@
|
|
|
704
704
|
<div class="device-info">
|
|
705
705
|
<h4>Android Tablet (10")</h4>
|
|
706
706
|
<div class="resolution">1600 x 2560 pixels</div>
|
|
707
|
-
<p>
|
|
708
|
-
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">
|
|
707
|
+
<p>Standard tablet dimensions for Google Play Tablet release requirements.</p>
|
|
708
|
+
<p style="margin-top: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">File: android_tablet_[path].png</p>
|
|
709
709
|
</div>
|
|
710
710
|
</div>
|
|
711
711
|
</div>
|
|
712
712
|
</section>
|
|
713
713
|
|
|
714
|
-
<!--
|
|
714
|
+
<!-- NPX Usage -->
|
|
715
715
|
<section class="glass-card">
|
|
716
|
-
<h2>š
|
|
717
|
-
<p style="color: var(--text-muted); margin-bottom: 1rem;">
|
|
716
|
+
<h2>š Instant Usage (NPX)</h2>
|
|
717
|
+
<p style="color: var(--text-muted); margin-bottom: 1rem;">You do not need any manual setup. Simply use <code>npx</code> to run MobileSnap instantly.</p>
|
|
718
718
|
|
|
719
719
|
<div class="terminal">
|
|
720
720
|
<div class="terminal-header">
|
|
@@ -728,38 +728,38 @@
|
|
|
728
728
|
<div class="terminal-body">
|
|
729
729
|
<button class="btn-copy" onclick="copyText('setup-cmds')">Copy</button>
|
|
730
730
|
<pre id="setup-cmds" style="font-family: inherit;">
|
|
731
|
-
<span class="terminal-comment"># 1.
|
|
731
|
+
<span class="terminal-comment"># 1. Run instantly using npx (change port according to your local server)</span>
|
|
732
732
|
<span class="terminal-prompt">PS></span>npx mobile-snap --url http://localhost:4321
|
|
733
733
|
|
|
734
|
-
<span class="terminal-comment"># 2.
|
|
734
|
+
<span class="terminal-comment"># 2. Optional: Download browser binaries if Playwright is not yet configured in the system</span>
|
|
735
735
|
<span class="terminal-prompt">PS></span>npx playwright install chromium</pre>
|
|
736
736
|
</div>
|
|
737
737
|
</div>
|
|
738
738
|
</section>
|
|
739
739
|
|
|
740
|
-
<!--
|
|
740
|
+
<!-- Command Builder -->
|
|
741
741
|
<section class="glass-card">
|
|
742
742
|
<h2>ā” CLI Command Builder (NPM / NPX)</h2>
|
|
743
|
-
<p style="color: var(--text-muted);">
|
|
743
|
+
<p style="color: var(--text-muted);">Use this interactive builder to generate and copy the command that fits your development needs:</p>
|
|
744
744
|
|
|
745
745
|
<div class="builder-container">
|
|
746
746
|
<div class="grid-2">
|
|
747
747
|
<div class="form-group">
|
|
748
|
-
<label for="input-url">
|
|
748
|
+
<label for="input-url">Local Dev Server URL</label>
|
|
749
749
|
<input type="text" id="input-url" class="form-control" value="http://localhost:4321" oninput="updateCommand()">
|
|
750
750
|
</div>
|
|
751
751
|
<div class="form-group">
|
|
752
|
-
<label for="input-paths">
|
|
752
|
+
<label for="input-paths">Routes / Paths (comma-separated)</label>
|
|
753
753
|
<input type="text" id="input-paths" class="form-control" value="/" oninput="updateCommand()">
|
|
754
754
|
</div>
|
|
755
755
|
</div>
|
|
756
756
|
<div class="grid-4">
|
|
757
757
|
<div class="form-group">
|
|
758
|
-
<label for="input-platform">Platform
|
|
758
|
+
<label for="input-platform">Target Platform</label>
|
|
759
759
|
<select id="input-platform" class="form-control" onchange="updateCommand()">
|
|
760
760
|
<option value="ios">iOS (App Store)</option>
|
|
761
761
|
<option value="android">Android (Play Store)</option>
|
|
762
|
-
<option value="both">Both (iOS & Android
|
|
762
|
+
<option value="both">Both (iOS & Android Simultaneously)</option>
|
|
763
763
|
</select>
|
|
764
764
|
</div>
|
|
765
765
|
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem; margin-top: 1.5rem;">
|
|
@@ -776,7 +776,7 @@
|
|
|
776
776
|
</div>
|
|
777
777
|
</div>
|
|
778
778
|
<div class="form-group" style="margin-top: 1rem;">
|
|
779
|
-
<label for="input-output">Folder
|
|
779
|
+
<label for="input-output">Output Folder</label>
|
|
780
780
|
<input type="text" id="input-output" class="form-control" value="mobilesnap_output" oninput="updateCommand()">
|
|
781
781
|
</div>
|
|
782
782
|
|
|
@@ -787,121 +787,121 @@
|
|
|
787
787
|
</div>
|
|
788
788
|
</section>
|
|
789
789
|
|
|
790
|
-
<!--
|
|
790
|
+
<!-- Parameters Table -->
|
|
791
791
|
<section class="glass-card">
|
|
792
|
-
<h2>āļø
|
|
792
|
+
<h2>āļø CLI Parameters & Options</h2>
|
|
793
793
|
<table class="option-table">
|
|
794
794
|
<thead>
|
|
795
795
|
<tr>
|
|
796
|
-
<th>
|
|
796
|
+
<th>Option</th>
|
|
797
797
|
<th>Alias</th>
|
|
798
|
-
<th>
|
|
799
|
-
<th>
|
|
798
|
+
<th>Description</th>
|
|
799
|
+
<th>Default</th>
|
|
800
800
|
</tr>
|
|
801
801
|
</thead>
|
|
802
802
|
<tbody>
|
|
803
803
|
<tr>
|
|
804
804
|
<td><span class="param-tag">--url</span></td>
|
|
805
805
|
<td><span class="param-tag">-u</span></td>
|
|
806
|
-
<td><strong>(
|
|
806
|
+
<td><strong>(Required)</strong> URL of the local development server (e.g., Astro).</td>
|
|
807
807
|
<td>-</td>
|
|
808
808
|
</tr>
|
|
809
809
|
<tr>
|
|
810
810
|
<td><span class="param-tag">--paths</span></td>
|
|
811
811
|
<td><span class="param-tag">-p</span></td>
|
|
812
|
-
<td>
|
|
812
|
+
<td>Comma-separated list of routes to capture.</td>
|
|
813
813
|
<td><code style="color: var(--secondary);">"/"</code></td>
|
|
814
814
|
</tr>
|
|
815
815
|
<tr>
|
|
816
816
|
<td><span class="param-tag">--platform</span></td>
|
|
817
817
|
<td><span class="param-tag">-l</span></td>
|
|
818
|
-
<td>
|
|
818
|
+
<td>Target platform to capture: <code style="color: var(--secondary);">"ios"</code>, <code style="color: var(--secondary);">"android"</code>, or <code style="color: var(--secondary);">"both"</code>.</td>
|
|
819
819
|
<td><code style="color: var(--secondary);">"ios"</code></td>
|
|
820
820
|
</tr>
|
|
821
821
|
<tr>
|
|
822
822
|
<td><span class="param-tag">--output</span></td>
|
|
823
823
|
<td><span class="param-tag">-o</span></td>
|
|
824
|
-
<td>
|
|
824
|
+
<td>Destination directory to save the screenshot files.</td>
|
|
825
825
|
<td><code style="color: var(--secondary);">"mobilesnap_output"</code></td>
|
|
826
826
|
</tr>
|
|
827
827
|
<tr>
|
|
828
828
|
<td><span class="param-tag">--crawl</span></td>
|
|
829
829
|
<td><span class="param-tag">-c</span></td>
|
|
830
|
-
<td>
|
|
830
|
+
<td>Automatically discover and screenshot all internal links starting from the home page.</td>
|
|
831
831
|
<td><code style="color: var(--secondary);">false</code></td>
|
|
832
832
|
</tr>
|
|
833
833
|
<tr>
|
|
834
834
|
<td><span class="param-tag">--detect-pages</span></td>
|
|
835
835
|
<td><span class="param-tag">-d</span></td>
|
|
836
|
-
<td>
|
|
836
|
+
<td>Scan the local project directories (Astro/Next.js) for static routes.</td>
|
|
837
837
|
<td><code style="color: var(--secondary);">false</code></td>
|
|
838
838
|
</tr>
|
|
839
839
|
<tr>
|
|
840
840
|
<td><span class="param-tag">--email</span></td>
|
|
841
841
|
<td>-</td>
|
|
842
|
-
<td>Email / Username
|
|
842
|
+
<td>Email / Username for automatic authentication if the page requires login.</td>
|
|
843
843
|
<td>-</td>
|
|
844
844
|
</tr>
|
|
845
845
|
<tr>
|
|
846
846
|
<td><span class="param-tag">--password</span></td>
|
|
847
847
|
<td>-</td>
|
|
848
|
-
<td>Password
|
|
848
|
+
<td>Password for automatic authentication if the page requires login (masked input).</td>
|
|
849
849
|
<td>-</td>
|
|
850
850
|
</tr>
|
|
851
851
|
<tr>
|
|
852
852
|
<td><span class="param-tag">--login-path</span></td>
|
|
853
853
|
<td>-</td>
|
|
854
|
-
<td>
|
|
854
|
+
<td>URL route path to the login page.</td>
|
|
855
855
|
<td><code style="color: var(--secondary);">"/login.html"</code></td>
|
|
856
856
|
</tr>
|
|
857
857
|
<tr>
|
|
858
858
|
<td><span class="param-tag">--html</span></td>
|
|
859
859
|
<td>-</td>
|
|
860
|
-
<td>
|
|
860
|
+
<td>Automatically append .html extension to detected static routes.</td>
|
|
861
861
|
<td><code style="color: var(--secondary);">false</code></td>
|
|
862
862
|
</tr>
|
|
863
863
|
<tr>
|
|
864
864
|
<td><span class="param-tag">--mockup</span></td>
|
|
865
865
|
<td><span class="param-tag">-m</span></td>
|
|
866
|
-
<td>
|
|
866
|
+
<td>Wrap screenshots in a beautiful device mockup frame (iPhone/Android) with status bars and transparent drop shadows.</td>
|
|
867
867
|
<td><code style="color: var(--secondary);">false</code></td>
|
|
868
868
|
</tr>
|
|
869
869
|
</tbody>
|
|
870
870
|
</table>
|
|
871
871
|
</section>
|
|
872
872
|
|
|
873
|
-
<!--
|
|
873
|
+
<!-- Resilience and Special Features -->
|
|
874
874
|
<section class="glass-card">
|
|
875
|
-
<h2>š”ļø
|
|
875
|
+
<h2>š”ļø System Resilience & Special Features</h2>
|
|
876
876
|
|
|
877
877
|
<div class="alert-box alert-warning">
|
|
878
878
|
<div class="alert-icon">š”</div>
|
|
879
879
|
<div class="alert-text">
|
|
880
|
-
<h5>
|
|
881
|
-
<p>MobileSnap
|
|
880
|
+
<h5>URL Protocol & Auto-Normalization</h5>
|
|
881
|
+
<p>MobileSnap automatically detects if the URL lacks a protocol prefix (e.g., <code>localhost:3000</code>) and prepends <code>http://</code> dynamically.</p>
|
|
882
882
|
</div>
|
|
883
883
|
</div>
|
|
884
884
|
|
|
885
885
|
<div class="alert-box alert-warning">
|
|
886
886
|
<div class="alert-icon">š</div>
|
|
887
887
|
<div class="alert-text">
|
|
888
|
-
<h5>User-
|
|
889
|
-
<p>
|
|
888
|
+
<h5>Dynamic User-Agents per Platform</h5>
|
|
889
|
+
<p>When capturing for Android, the system emulates Android 13 (Pixel 7) user-agent. For iOS, it emulates iPhone. This guarantees that your server serves the appropriate layout for each specific vendor.</p>
|
|
890
890
|
</div>
|
|
891
891
|
</div>
|
|
892
892
|
|
|
893
893
|
<div class="alert-box alert-warning">
|
|
894
894
|
<div class="alert-icon">š</div>
|
|
895
895
|
<div class="alert-text">
|
|
896
|
-
<h5>
|
|
897
|
-
<p>
|
|
896
|
+
<h5>Retina Resolution Scale (DPI 3x)</h5>
|
|
897
|
+
<p>All screenshots are captured with <code>deviceScaleFactor: 3</code> to ensure the output remains ultra-sharp, complying with official store upload standards.</p>
|
|
898
898
|
</div>
|
|
899
899
|
</div>
|
|
900
900
|
</section>
|
|
901
901
|
</div>
|
|
902
902
|
|
|
903
903
|
<footer>
|
|
904
|
-
<p>© 2026 MobileSnap CLI Tool.
|
|
904
|
+
<p>© 2026 MobileSnap CLI Tool. Crafted with visual precision for all your App Store & Play Store requirements.</p>
|
|
905
905
|
</footer>
|
|
906
906
|
|
|
907
907
|
<script>
|
|
@@ -942,9 +942,9 @@
|
|
|
942
942
|
const textToCopy = isInner ? element.innerText : element.textContent;
|
|
943
943
|
|
|
944
944
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
945
|
-
alert('
|
|
945
|
+
alert('Command copied to clipboard successfully!');
|
|
946
946
|
}).catch(err => {
|
|
947
|
-
console.error('
|
|
947
|
+
console.error('Failed to copy text: ', err);
|
|
948
948
|
});
|
|
949
949
|
}
|
|
950
950
|
</script>
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# MobileSnap šø
|
|
2
2
|
|
|
3
|
-
MobileSnap
|
|
3
|
+
MobileSnap is a Node.js-based CLI (Command Line Interface) tool designed to automate the capture of pixel-precise App Store and Google Play Store screenshots directly from local development servers (such as Astro, Next.js, React, or Vue).
|
|
4
4
|
|
|
5
|
-
### š±
|
|
5
|
+
### š± Sample Output (Premium Mockups)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Below is a real-world visualization of screenshots automatically wrapped in premium device mockup frames (using the `-m` or `--mockup` option):
|
|
8
8
|
|
|
9
9
|
| iOS (iPhone 6.7" Pro Max) | Android Phone (Google Pixel 7) |
|
|
10
10
|
| :---: | :---: |
|
|
@@ -12,148 +12,147 @@ Berikut adalah visualisasi nyata dari tangkapan layar yang dibungkus otomatis ke
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## šļø
|
|
15
|
+
## šļø System Architecture
|
|
16
16
|
|
|
17
|
-
MobileSnap
|
|
17
|
+
MobileSnap is designed with a focus on efficiency, reliability, and ease of use. Below is the main workflow diagram of the application:
|
|
18
18
|
|
|
19
19
|
```mermaid
|
|
20
20
|
graph TD
|
|
21
|
-
A[
|
|
22
|
-
B --> C{
|
|
23
|
-
C -->|URL
|
|
24
|
-
C -->|URL
|
|
25
|
-
D --> F[
|
|
26
|
-
F --> G[
|
|
27
|
-
G --> H[
|
|
28
|
-
H --> I[
|
|
29
|
-
I --> J[
|
|
30
|
-
J --> K[
|
|
31
|
-
K --> L[
|
|
32
|
-
L --> M[
|
|
21
|
+
A[User runs npx CLI command] --> B[Commander CLI Parser]
|
|
22
|
+
B --> C{Validate Arguments & URL}
|
|
23
|
+
C -->|Valid URL| D[Initialize Playwright Async]
|
|
24
|
+
C -->|Invalid URL| E[Console Error & Exit]
|
|
25
|
+
D --> F[Launch Headless Chromium Browser]
|
|
26
|
+
F --> G[Loop through Platform Choices: iOS / Android / Both]
|
|
27
|
+
G --> H[Configure Device Emulator Context & Viewport]
|
|
28
|
+
H --> I[Loop through URL Paths/Routes]
|
|
29
|
+
I --> J[Navigate to Page & Wait for Network Idle]
|
|
30
|
+
J --> K[Capture Screenshot & Save File]
|
|
31
|
+
K --> L[Close Browser Context]
|
|
32
|
+
L --> M[Done & Display Summary]
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
###
|
|
35
|
+
### Main Components
|
|
36
36
|
|
|
37
|
-
1. **Parser
|
|
38
|
-
2. **
|
|
39
|
-
3. **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
4. **
|
|
37
|
+
1. **CLI Parser ([bin/cli.js](file:///d:/Deweb/MobileSnap/bin/cli.js))**: Uses the `Commander` library to process user input parameters intuitively.
|
|
38
|
+
2. **Browser Automation Engine**: Powered by `Playwright` to run headless Chromium instances.
|
|
39
|
+
3. **Precision Emulator Configuration**:
|
|
40
|
+
- **Device Scale (DPI)**: Set to `deviceScaleFactor: 3` to produce ultra-sharp screenshots (Retina/High DPI) meeting official store release guidelines.
|
|
41
|
+
- **User Agent**: Configured dynamically based on the target platform (iOS uses an iPhone user agent; Android uses a Google Pixel 7 user agent).
|
|
42
|
+
4. **Web Hydration Synchronization**: Employs `page.waitForLoadState("networkidle")` to detect when all page assets are fully loaded before capturing the screenshot. This is crucial for modern frameworks like Astro.
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
-
## š±
|
|
46
|
+
## š± Target Dimension Specifications
|
|
47
47
|
|
|
48
|
-
MobileSnap
|
|
48
|
+
MobileSnap automatically captures screenshots for the following devices based on the selected platform:
|
|
49
49
|
|
|
50
50
|
### iOS (Apple App Store)
|
|
51
|
-
|
|
|
51
|
+
| Display Name | Resolution (Pixels) | Aspect Ratio | Sample Output File |
|
|
52
52
|
| :--- | :--- | :--- | :--- |
|
|
53
53
|
| **6.7" Display** | 1290 x 2796 | 19.5:9 | `6.7_inch_home.png` |
|
|
54
54
|
| **6.5" Display** | 1242 x 2688 | 19.5:9 | `6.5_inch_home.png` |
|
|
55
55
|
|
|
56
56
|
### Android (Google Play Store)
|
|
57
|
-
|
|
|
57
|
+
| Display Name | Resolution (Pixels) | Aspect Ratio | Sample Output File |
|
|
58
58
|
| :--- | :--- | :--- | :--- |
|
|
59
59
|
| **Android Phone** | 1080 x 2400 | 20:9 | `android_phone_home.png` |
|
|
60
60
|
| **Android Tablet (10")** | 1600 x 2560 | 16:10 | `android_tablet_home.png` |
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
|
-
## š
|
|
64
|
+
## š Instant Usage (NPX)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
You do not need to install anything permanently. Simply run the command using `npx`:
|
|
67
67
|
|
|
68
68
|
```powershell
|
|
69
|
-
# 1.
|
|
69
|
+
# 1. Run directly against your local dev server
|
|
70
70
|
npx mobile-snap --url http://localhost:4321
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
> [!NOTE]
|
|
74
|
-
>
|
|
74
|
+
> If this is your first time running Playwright, you may need to download the browser binaries by running:
|
|
75
75
|
> ```powershell
|
|
76
76
|
> npx playwright install chromium
|
|
77
77
|
> ```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
To install it globally on your system:
|
|
80
80
|
```powershell
|
|
81
81
|
npm install -g mobile-snap
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## š»
|
|
86
|
+
## š» CLI Usage Guide
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
The application accepts the following main options:
|
|
89
89
|
|
|
90
|
-
|
|
|
90
|
+
| Option | Alias | Description | Default | Choices |
|
|
91
91
|
| :--- | :--- | :--- | :--- | :--- |
|
|
92
|
-
| `--url` | `-u` | **(
|
|
93
|
-
| `--paths` | `-p` |
|
|
94
|
-
| `--output`| `-o` |
|
|
95
|
-
| `--platform`| `-l`|
|
|
96
|
-
| `--crawl` | `-c` |
|
|
97
|
-
| `--detect-pages` | `-d` |
|
|
98
|
-
| `--email` | - | Email
|
|
99
|
-
| `--password` | - | Password
|
|
100
|
-
| `--login-path`| - |
|
|
101
|
-
| `--html` | - |
|
|
102
|
-
| `--mockup` | `-m` |
|
|
103
|
-
|
|
104
|
-
###
|
|
105
|
-
|
|
106
|
-
#### 1.
|
|
92
|
+
| `--url` | `-u` | **(Required)** Local development server URL. | - | - |
|
|
93
|
+
| `--paths` | `-p` | Comma-separated list of routes to capture. | `/` | - |
|
|
94
|
+
| `--output`| `-o` | Name of the directory to save screenshots. | `mobilesnap_output` | - |
|
|
95
|
+
| `--platform`| `-l`| Target platform for screenshots. | `ios` | `ios`, `android`, `both` |
|
|
96
|
+
| `--crawl` | `-c` | Enable automatic discovery (crawling) of internal links on the home page. | `false` | - |
|
|
97
|
+
| `--detect-pages` | `-d` | Scan local project pages directory (`src/pages` or `pages`) for static routes. | `false` | - |
|
|
98
|
+
| `--email` | - | Email for automatic authentication. | - | - |
|
|
99
|
+
| `--password` | - | Password for automatic authentication (hidden in terminal). | - | - |
|
|
100
|
+
| `--login-path`| - | Route path to the login page. | `/login.html` | - |
|
|
101
|
+
| `--html` | - | Automatically append `.html` extension to detected static routes. | `false` | - |
|
|
102
|
+
| `--mockup` | `-m` | Wrap screenshots in beautiful, premium device mockup frames (iPhone/Android) with status bars and transparent drop shadows. | `false` | - |
|
|
103
|
+
|
|
104
|
+
### Command Examples
|
|
105
|
+
|
|
106
|
+
#### 1. Capture iOS Only (Default)
|
|
107
107
|
```powershell
|
|
108
108
|
npx mobile-snap --url http://localhost:4321
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
#### 2.
|
|
111
|
+
#### 2. Capture Android Only with Device Mockup Frame
|
|
112
112
|
```powershell
|
|
113
113
|
npx mobile-snap --url http://localhost:4321 --platform android --mockup
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
#### 3.
|
|
117
|
-
|
|
116
|
+
#### 3. Capture Specific Routes for Both Platforms
|
|
117
|
+
Captures the home page `/` and page `/scan` for both platforms simultaneously into the `hasil_store` directory:
|
|
118
118
|
```powershell
|
|
119
119
|
npx mobile-snap --url http://localhost:4321 --paths "/, /scan" --platform both --output hasil_store
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
#### 4. Auto-Crawl
|
|
123
|
-
|
|
122
|
+
#### 4. Auto-Crawl Website & Interactive Login with Device Mockup Frame
|
|
123
|
+
Recursively crawls all internal links from the home page and captures each discovered page with a mockup frame:
|
|
124
124
|
```powershell
|
|
125
125
|
npx mobile-snap --url http://localhost:4321 --crawl --platform both --mockup
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Jika Anda berada di dalam root direktori proyek Astro Anda, jalankan perintah ini untuk mendeteksi secara otomatis semua rute halaman statis dari folder `src/pages` dengan login otomatis:
|
|
128
|
+
#### 5. Auto-Detect Local Project Routes (Astro / Next.js) with Auto-Login
|
|
129
|
+
If you are inside the root directory of your Astro/Next.js project, run this command to automatically detect all static page routes from the pages folder and capture them with auto-login:
|
|
131
130
|
```powershell
|
|
132
131
|
npx mobile-snap --url http://localhost:4321 --detect-pages --html --email "user@email.com" --password "rahasia"
|
|
133
132
|
```
|
|
134
133
|
|
|
135
134
|
---
|
|
136
135
|
|
|
137
|
-
## š
|
|
136
|
+
## š Automatic & Interactive Authentication
|
|
138
137
|
|
|
139
|
-
MobileSnap
|
|
138
|
+
MobileSnap intelligently distinguishes between public pages and protected pages (requiring authentication) based on client-side redirects to the login route.
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
1. **
|
|
143
|
-
2. **Auto-Login**: MobileSnap
|
|
144
|
-
3. **
|
|
140
|
+
If a route requiring login is detected, MobileSnap will:
|
|
141
|
+
1. **Prompt for Credentials Interactively**: If `--email` and/or `--password` options are not supplied via the CLI, the tool will prompt for them in the terminal, masking the password input for security.
|
|
142
|
+
2. **Auto-Login**: MobileSnap performs the sign-in sequence before capturing screenshots for all protected routes.
|
|
143
|
+
3. **Post-Login Crawl**: If the `--crawl` option is enabled, MobileSnap will also discover and crawl internal links that only become visible in the dashboard post-authentication.
|
|
145
144
|
|
|
146
145
|
---
|
|
147
146
|
|
|
148
|
-
## ā¹ļø
|
|
147
|
+
## ā¹ļø Command Help (`--help`)
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
You can display the help information directly from the terminal by running:
|
|
151
150
|
|
|
152
151
|
```powershell
|
|
153
152
|
npx mobile-snap --help
|
|
154
153
|
```
|
|
155
154
|
|
|
156
|
-
|
|
155
|
+
Official help output:
|
|
157
156
|
```text
|
|
158
157
|
Usage: mobile-snap [options]
|
|
159
158
|
|
|
@@ -161,18 +160,24 @@ Usage: mobile-snap [options]
|
|
|
161
160
|
|
|
162
161
|
Options:
|
|
163
162
|
-V, --version output the version number
|
|
164
|
-
-u, --url <url> Base URL of the local development server (e.g.
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
-
|
|
169
|
-
|
|
163
|
+
-u, --url <url> Base URL of the local development server (e.g.
|
|
164
|
+
localhost:3000)
|
|
165
|
+
-p, --paths <paths> Comma-separated list of routes to capture
|
|
166
|
+
(default: "/")
|
|
167
|
+
-o, --output <output> Output directory to save screenshots (default:
|
|
168
|
+
"mobilesnap_output")
|
|
169
|
+
-l, --platform <platform> Target platform: "ios", "android", or "both"
|
|
170
|
+
(default: "ios")
|
|
171
|
+
-c, --crawl Discover and screenshot all internal links
|
|
172
|
+
automatically (default: false)
|
|
173
|
+
-d, --detect-pages Scan local project pages directory (src/pages or
|
|
174
|
+
pages) for static routes (default: false)
|
|
170
175
|
--email <email> Email for automatic login authentication
|
|
171
176
|
--password <password> Password for automatic login authentication
|
|
172
177
|
--login-path <path> Path to the login page (default: "/login.html")
|
|
173
|
-
--html Auto append .html extension to detected routes
|
|
174
|
-
|
|
178
|
+
--html Auto append .html extension to detected routes
|
|
179
|
+
(default: false)
|
|
180
|
+
-m, --mockup Wrap screenshots in a beautiful iPhone/Android
|
|
181
|
+
device mockup frame (default: false)
|
|
175
182
|
-h, --help display help for command
|
|
176
183
|
```
|
|
177
|
-
|
|
178
|
-
|
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import readline from 'readline';
|
|
10
10
|
|
|
11
|
-
// Device configurations
|
|
11
|
+
// Device configurations with logical dimensions (CSS pixels) and device scale factor for precise physical resolution
|
|
12
12
|
const DEVICE_CONFIGS = {
|
|
13
13
|
ios: {
|
|
14
14
|
devices: {
|
|
@@ -68,7 +68,7 @@ function safeFilename(route) {
|
|
|
68
68
|
return cleanPath.replace(/[^a-zA-Z0-9_\-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Helper function to scan the src/pages or pages directory (for Astro, Next.js, etc.)
|
|
72
72
|
function detectLocalPages(dir = process.cwd()) {
|
|
73
73
|
const pagesDirs = [
|
|
74
74
|
path.join(dir, 'src', 'pages'),
|
|
@@ -100,11 +100,11 @@ function detectLocalPages(dir = process.cwd()) {
|
|
|
100
100
|
if (name === 'index') {
|
|
101
101
|
route = baseRoute || '/';
|
|
102
102
|
}
|
|
103
|
-
//
|
|
103
|
+
// Ignore dynamic routes containing [ or ]
|
|
104
104
|
if (!route.includes('[') && !route.includes(']')) {
|
|
105
|
-
//
|
|
105
|
+
// Ensure it starts with /
|
|
106
106
|
let finalRoute = '/' + route.replace(/^\/+/, '');
|
|
107
|
-
//
|
|
107
|
+
// Ignore API routes (server endpoints)
|
|
108
108
|
if (!finalRoute.startsWith('/api/')) {
|
|
109
109
|
routes.push(finalRoute);
|
|
110
110
|
}
|
|
@@ -118,7 +118,7 @@ function detectLocalPages(dir = process.cwd()) {
|
|
|
118
118
|
return [...new Set(routes)];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//
|
|
121
|
+
// Helper function to crawl internal links starting from the home page
|
|
122
122
|
async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, loginPath, addHtml, crawl) {
|
|
123
123
|
const normLoginPath = '/' + loginPath.replace(/^\/+/, '');
|
|
124
124
|
const publicRoutes = new Set();
|
|
@@ -142,10 +142,10 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
142
142
|
console.error(pc.red(` [Crawler Error] ${err.message}`));
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
const nonAuthSpinner = ora('
|
|
145
|
+
const nonAuthSpinner = ora('Analyzing public routes and detecting authentication requirements...').start();
|
|
146
146
|
const routesToCheck = Array.from(allDetectedRoutes);
|
|
147
147
|
|
|
148
|
-
//
|
|
148
|
+
// If the initial path list is empty, default to the root path '/'
|
|
149
149
|
if (routesToCheck.length === 0) {
|
|
150
150
|
routesToCheck.push('/');
|
|
151
151
|
allDetectedRoutes.add('/');
|
|
@@ -163,7 +163,7 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
163
163
|
await page.goto(targetUrl, { timeout: 15000 });
|
|
164
164
|
await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
|
|
165
165
|
|
|
166
|
-
//
|
|
166
|
+
// Wait dynamically for client-side redirects (such as splash screen to login) to complete
|
|
167
167
|
if (cleanRoute === '/' || cleanRoute.includes('splash')) {
|
|
168
168
|
await page.waitForURL(u => {
|
|
169
169
|
const pathname = u.pathname;
|
|
@@ -181,12 +181,12 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
181
181
|
finalPath = finalUrl;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
//
|
|
184
|
+
// Normalize finalPath to end with .html if the flag is active
|
|
185
185
|
if (addHtml && finalPath !== '/' && !finalPath.endsWith('.html') && !/\.[a-z0-9]+$/i.test(finalPath)) {
|
|
186
186
|
finalPath += '.html';
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// Check if a login form or credential inputs are present in the current page DOM
|
|
190
190
|
const hasLoginForm = await page.evaluate(() => {
|
|
191
191
|
const hasUser = !!document.querySelector('#username, input[type="email"], input[name="username"], input[name="login"]');
|
|
192
192
|
const hasPass = !!document.querySelector('#password, input[type="password"], input[name="password"]');
|
|
@@ -217,7 +217,7 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
217
217
|
publicRoutes.add(cleanRoute);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
//
|
|
220
|
+
// Always crawl public links if crawling is active and the current page does not require authentication
|
|
221
221
|
if (crawl && !authRoutes.has(cleanRoute)) {
|
|
222
222
|
const hrefs = await page.evaluate(() => {
|
|
223
223
|
return Array.from(document.querySelectorAll('a'))
|
|
@@ -253,32 +253,32 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
} catch (err) {
|
|
256
|
-
ora().warn(pc.yellow(`
|
|
256
|
+
ora().warn(pc.yellow(`Failed to analyze route ${cleanRoute}: ${err.message}`));
|
|
257
257
|
publicRoutes.add(cleanRoute);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
await context.close();
|
|
262
|
-
nonAuthSpinner.succeed(`
|
|
262
|
+
nonAuthSpinner.succeed(`Initial analysis complete. Detected ${publicRoutes.size} public routes and ${authRoutes.size} routes requiring authentication.`);
|
|
263
263
|
|
|
264
|
-
// 2.
|
|
264
|
+
// 2. Prompt for credentials interactively if there are authenticated routes and email/password are empty
|
|
265
265
|
let finalEmail = email;
|
|
266
266
|
let finalPassword = password;
|
|
267
267
|
|
|
268
268
|
if (authRoutes.size > 0 && (!finalEmail || !finalPassword)) {
|
|
269
|
-
console.log(pc.yellow(`\nš
|
|
269
|
+
console.log(pc.yellow(`\nš Detected ${authRoutes.size} pages requiring login.`));
|
|
270
270
|
if (!finalEmail) {
|
|
271
|
-
finalEmail = await promptUser('š
|
|
271
|
+
finalEmail = await promptUser('š Enter Email/Username: ');
|
|
272
272
|
}
|
|
273
273
|
if (!finalPassword) {
|
|
274
|
-
finalPassword = await promptUser('š
|
|
275
|
-
console.log(''); //
|
|
274
|
+
finalPassword = await promptUser('š Enter Password (hidden input): ', true);
|
|
275
|
+
console.log(''); // new line after pressing enter
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
// 3.
|
|
279
|
+
// 3. Phase Two Crawl (Post-Login) to discover internal dashboard pages
|
|
280
280
|
if (authRoutes.size > 0 && finalEmail && finalPassword) {
|
|
281
|
-
const authCrawlSpinner = ora('
|
|
281
|
+
const authCrawlSpinner = ora('Performing login and crawling authenticated internal pages...').start();
|
|
282
282
|
const authContext = await browser.newContext();
|
|
283
283
|
const authPage = await authContext.newPage();
|
|
284
284
|
|
|
@@ -333,7 +333,7 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
if (!loginSuccess) {
|
|
336
|
-
throw new Error(loginError || 'Timeout: URL
|
|
336
|
+
throw new Error(loginError || 'Timeout: Page URL did not change from the login page after 10 seconds.');
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
if (crawl) {
|
|
@@ -378,13 +378,13 @@ async function analyzeRoutes(browser, baseUrl, initialPaths, email, password, lo
|
|
|
378
378
|
} catch (e) {}
|
|
379
379
|
}
|
|
380
380
|
} catch (routeErr) {
|
|
381
|
-
//
|
|
381
|
+
// Ignore if a specific route fails to load
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
|
-
authCrawlSpinner.succeed(`
|
|
385
|
+
authCrawlSpinner.succeed(`Post-login crawl complete. Found a total of ${authRoutes.size} authenticated routes.`);
|
|
386
386
|
} catch (err) {
|
|
387
|
-
authCrawlSpinner.fail(`
|
|
387
|
+
authCrawlSpinner.fail(`Failed to crawl authenticated pages: ${err.message}`);
|
|
388
388
|
} finally {
|
|
389
389
|
await authContext.close();
|
|
390
390
|
}
|
|
@@ -681,14 +681,14 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
681
681
|
|
|
682
682
|
let finalPaths = [...paths];
|
|
683
683
|
|
|
684
|
-
// 1. Auto-detect
|
|
684
|
+
// 1. Auto-detect local pages if enabled
|
|
685
685
|
if (detectPages) {
|
|
686
686
|
const localRoutes = detectLocalPages();
|
|
687
687
|
if (localRoutes && localRoutes.length > 0) {
|
|
688
|
-
console.log(pc.green(`š
|
|
688
|
+
console.log(pc.green(`š Detected local pages folder. Adding ${localRoutes.length} static routes.`));
|
|
689
689
|
finalPaths = [...new Set([...finalPaths, ...localRoutes])];
|
|
690
690
|
} else {
|
|
691
|
-
console.log(pc.yellow(`ā Folder 'src/pages'
|
|
691
|
+
console.log(pc.yellow(`ā Folder 'src/pages' or 'pages' not found in the current directory.`));
|
|
692
692
|
}
|
|
693
693
|
}
|
|
694
694
|
|
|
@@ -697,12 +697,12 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
697
697
|
console.log(`Platform(s): ${pc.cyan(targetPlatforms.join(', ').toUpperCase())}`);
|
|
698
698
|
console.log(`Output Directory: ${pc.cyan(path.resolve(outputDir))}\n`);
|
|
699
699
|
|
|
700
|
-
//
|
|
700
|
+
// Ensure the output directory exists
|
|
701
701
|
if (!fs.existsSync(outputDir)) {
|
|
702
702
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
-
//
|
|
705
|
+
// Launch browser
|
|
706
706
|
let browser;
|
|
707
707
|
const launchSpinner = ora('Launching Chromium browser...').start();
|
|
708
708
|
try {
|
|
@@ -714,33 +714,33 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
714
714
|
} catch (err) {
|
|
715
715
|
launchSpinner.fail(pc.red('Failed to launch Chromium browser'));
|
|
716
716
|
console.error(pc.red(err.message));
|
|
717
|
-
console.log(pc.yellow('\nš”
|
|
717
|
+
console.log(pc.yellow('\nš” Tip: Run "npx playwright install chromium" to download browser binaries.'));
|
|
718
718
|
process.exit(1);
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
-
//
|
|
721
|
+
// Call analyzeRoutes to separate public and authenticated routes
|
|
722
722
|
let result;
|
|
723
723
|
try {
|
|
724
724
|
result = await analyzeRoutes(browser, url, finalPaths, email, password, loginPath, addHtml, crawl);
|
|
725
725
|
} catch (err) {
|
|
726
|
-
console.error(pc.red(`
|
|
726
|
+
console.error(pc.red(`Failed to analyze routes: ${err.message}`));
|
|
727
727
|
await browser.close();
|
|
728
728
|
process.exit(1);
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
const { publicRoutes, authRoutes, email: finalEmail, password: finalPassword } = result;
|
|
732
732
|
|
|
733
|
-
console.log(pc.bold(`\
|
|
733
|
+
console.log(pc.bold(`\nDetected Public Routes (${publicRoutes.length}):`));
|
|
734
734
|
publicRoutes.forEach(p => console.log(` - ${pc.green(p)}`));
|
|
735
735
|
|
|
736
736
|
if (authRoutes.length > 0) {
|
|
737
|
-
console.log(pc.bold(`\
|
|
737
|
+
console.log(pc.bold(`\nDetected Routes Requiring Authentication (${authRoutes.length}):`));
|
|
738
738
|
authRoutes.forEach(p => console.log(` - ${pc.cyan(p)}`));
|
|
739
739
|
}
|
|
740
740
|
console.log('');
|
|
741
741
|
|
|
742
742
|
if (publicRoutes.length === 0 && authRoutes.length === 0) {
|
|
743
|
-
console.log(pc.yellow('
|
|
743
|
+
console.log(pc.yellow('No routes were found to capture.'));
|
|
744
744
|
await browser.close();
|
|
745
745
|
return;
|
|
746
746
|
}
|
|
@@ -816,9 +816,9 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
816
816
|
console.error(pc.red(` [Browser Error] ${err.message}`));
|
|
817
817
|
});
|
|
818
818
|
|
|
819
|
-
// ---
|
|
819
|
+
// --- Phase 1: Capturing Public Pages (No Login) ---
|
|
820
820
|
if (publicRoutes.length > 0) {
|
|
821
|
-
console.log(` šø
|
|
821
|
+
console.log(` šø Capturing public pages...`);
|
|
822
822
|
for (const route of publicRoutes) {
|
|
823
823
|
const targetUrl = `${url}${route}`;
|
|
824
824
|
const nameSnippet = safeFilename(route);
|
|
@@ -841,7 +841,7 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
// ---
|
|
844
|
+
// --- Phase 2: Capturing Authenticated Pages ---
|
|
845
845
|
if (authRoutes.length > 0) {
|
|
846
846
|
if (finalEmail && finalPassword) {
|
|
847
847
|
const loginSpinner = ora(` Logging in to session for ${deviceName}...`).start();
|
|
@@ -856,7 +856,7 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
856
856
|
const submitBtn = await page.locator('button[type="submit"], button.save-btn').first();
|
|
857
857
|
await submitBtn.click();
|
|
858
858
|
|
|
859
|
-
//
|
|
859
|
+
// Wait for login success (URL change) or an error message to appear
|
|
860
860
|
let loginSuccess = false;
|
|
861
861
|
let loginError = '';
|
|
862
862
|
for (let i = 0; i < 40; i++) {
|
|
@@ -882,12 +882,12 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
882
882
|
}
|
|
883
883
|
|
|
884
884
|
if (!loginSuccess) {
|
|
885
|
-
throw new Error(loginError || 'Timeout: URL
|
|
885
|
+
throw new Error(loginError || 'Timeout: Page URL did not change from the login page after 10 seconds.');
|
|
886
886
|
}
|
|
887
887
|
|
|
888
888
|
loginSpinner.succeed(` Logged in successfully for ${deviceName}`);
|
|
889
889
|
|
|
890
|
-
console.log(` šø
|
|
890
|
+
console.log(` šø Capturing authenticated pages...`);
|
|
891
891
|
for (const route of authRoutes) {
|
|
892
892
|
const targetUrl = `${url}${route}`;
|
|
893
893
|
const nameSnippet = safeFilename(route);
|
|
@@ -910,7 +910,7 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
910
910
|
}
|
|
911
911
|
} catch (loginErr) {
|
|
912
912
|
loginSpinner.fail(pc.red(` Auto-login failed for ${deviceName}: ${loginErr.message}`));
|
|
913
|
-
console.log(pc.yellow(` š”
|
|
913
|
+
console.log(pc.yellow(` š” Continuing to capture authenticated routes without login (may be redirected to login/splash).`));
|
|
914
914
|
|
|
915
915
|
for (const route of authRoutes) {
|
|
916
916
|
const targetUrl = `${url}${route}`;
|
|
@@ -934,7 +934,7 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
934
934
|
}
|
|
935
935
|
}
|
|
936
936
|
} else {
|
|
937
|
-
console.log(pc.yellow(` ā
|
|
937
|
+
console.log(pc.yellow(` ā Skipping login (empty credentials). Capturing authenticated routes without login.`));
|
|
938
938
|
for (const route of authRoutes) {
|
|
939
939
|
const targetUrl = `${url}${route}`;
|
|
940
940
|
const nameSnippet = safeFilename(route);
|
|
@@ -963,13 +963,13 @@ async function captureScreenshots(url, paths, outputDir, platform, crawl, detect
|
|
|
963
963
|
}
|
|
964
964
|
|
|
965
965
|
await browser.close();
|
|
966
|
-
console.log(pc.bold(pc.green(`\nš
|
|
966
|
+
console.log(pc.bold(pc.green(`\nš Finished! All screenshots successfully saved in '${outputDir}'.`)));
|
|
967
967
|
}
|
|
968
968
|
|
|
969
969
|
program
|
|
970
970
|
.name('mobile-snap')
|
|
971
971
|
.description('ā” MobileSnap CLI: Automate App Store & Google Play Store screenshots')
|
|
972
|
-
.version('1.0.
|
|
972
|
+
.version('1.0.5')
|
|
973
973
|
.requiredOption('-u, --url <url>', 'Base URL of the local development server (e.g. localhost:3000)')
|
|
974
974
|
.option('-p, --paths <paths>', 'Comma-separated list of routes to capture', '/')
|
|
975
975
|
.option('-o, --output <output>', 'Output directory to save screenshots', 'mobilesnap_output')
|
|
@@ -989,7 +989,7 @@ program
|
|
|
989
989
|
|
|
990
990
|
const platformVal = options.platform.toLowerCase();
|
|
991
991
|
if (!['ios', 'android', 'both'].includes(platformVal)) {
|
|
992
|
-
console.error(pc.red(`Error:
|
|
992
|
+
console.error(pc.red(`Error: Invalid platform '${options.platform}'. Choose 'ios', 'android', or 'both'.`));
|
|
993
993
|
process.exit(1);
|
|
994
994
|
}
|
|
995
995
|
|
|
@@ -1006,7 +1006,7 @@ program
|
|
|
1006
1006
|
options.html,
|
|
1007
1007
|
options.mockup
|
|
1008
1008
|
).catch(err => {
|
|
1009
|
-
console.error(pc.red(`
|
|
1009
|
+
console.error(pc.red(`An unexpected error occurred: ${err.message}`));
|
|
1010
1010
|
process.exit(1);
|
|
1011
1011
|
});
|
|
1012
1012
|
});
|